Skip to content

Remove license requirement for React on Rails Pro - use HTML attribution comment#2324

Merged
justin808 merged 31 commits intomasterfrom
abanoub/2323-license-optional-attribution
Feb 1, 2026
Merged

Remove license requirement for React on Rails Pro - use HTML attribution comment#2324
justin808 merged 31 commits intomasterfrom
abanoub/2323-license-optional-attribution

Conversation

@AbanoubGhadban
Copy link
Copy Markdown
Collaborator

@AbanoubGhadban AbanoubGhadban commented Jan 28, 2026

Summary

Changes React on Rails Pro licensing model from "license-required" to "license-optional with attribution". The app now runs regardless of license status, displaying an HTML comment indicating the license state and logging a warning for license issues in production.

Addresses #2323

Changes

Behavior Changes

Scenario Before After
No license App crashes on startup App runs, logs warning (prod) or info (dev/test), shows UNLICENSED comment
Expired license App crashes (or grace period) App runs, logs warning (prod) or info (dev/test), shows LICENSE EXPIRED comment
Invalid license App crashes App runs, logs warning (prod) or info (dev/test), shows INVALID LICENSE comment
Valid license App runs App runs, logs info, shows Licensed comment

What's Removed

  • Free evaluation license concept — No longer needed
  • Grace period logic — No longer relevant since app never crashes
  • Application crash on invalid license — Replaced with warning logs
  • Unused exports/methodslicensed?, license_data (Ruby); isLicensed(), getLicenseData() (TS) had no production callers and have been removed along with their caching logic (@license_data/cachedLicenseData) and logLicenseInfo methods
  • console.warn/console.log in TS license validator — Replaced with Pino logger (log.warn)

HTML Attribution Comments

<!-- Powered by React on Rails Pro (c) ShakaCode | Licensed -->
<!-- Powered by React on Rails Pro (c) ShakaCode | LICENSE EXPIRED -->
<!-- Powered by React on Rails Pro (c) ShakaCode | INVALID LICENSE -->
<!-- Powered by React on Rails Pro (c) ShakaCode | UNLICENSED -->

License Logging

License issues are logged at startup with environment-aware messages:

Production:

  • Rails.logger.warn with violation message and license URL
  • Example: [React on Rails Pro] No license found. Using React on Rails Pro in production without a valid license violates the license terms. Get a license at https://www.shakacode.com/react-on-rails-pro/

Development/Test:

  • Rails.logger.info with reassuring message (no warning)
  • Example: [React on Rails Pro] No license found. No license required for development/test environments.

TypeScript (Node renderer):

  • Pino log.warn for license problems (no environment detection available)

Thread Safety

  • Ruby LicenseValidator.license_status now uses a Mutex to prevent race conditions in multi-threaded environments
  • Uses double-check locking pattern for efficient caching

Files Changed

Ruby:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb — New status-based API with thread-safe caching; removed licensed?, license_data, log_license_info, and @license_data caching
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb — Environment-aware log messages (warn in prod, info in dev/test)
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb — Attribution comment; removed licensed? delegator
  • react_on_rails/lib/react_on_rails/utils.rb — Check gem presence only

TypeScript:

  • packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts — Non-blocking validation; improved error handling for non-Error exceptions; removed isLicensed(), getLicenseData(), logLicenseInfo(), cachedLicenseData; replaced console.warn with Pino logger
  • packages/react-on-rails-pro-node-renderer/src/master.ts — Use new API

Tests:

  • license_validator_spec.rb — Removed .licensed?, .license_data test blocks; added thread safety test
  • engine_spec.rb — New integration tests for environment-aware license logging
  • licenseValidator.test.ts — Removed isLicensed/getLicenseData tests; added test for error message logging; mock Pino logger instead of console.warn
  • utils_spec.rb — Updated for new API

Config:

  • knip.ts — Updated comment for licenseValidator entry point

Test plan

  • Verify app starts without license in production (shows UNLICENSED comment, logs warning)
  • Verify app starts without license in development (shows UNLICENSED comment, logs info - no warning)
  • Verify app starts with expired license (shows LICENSE EXPIRED comment)
  • Verify app starts with valid license (shows Licensed comment, logs success info)
  • Verify Node renderer starts without license (logs warning)
  • Run full test suite

Summary by CodeRabbit

  • New Features

    • License-optional model: development/CI can run without a license; production requires a paid license.
    • New status-based license API (valid / expired / invalid / missing) with cached status and reset support.
  • Behavior

    • Non-blocking, deterministic status checks; environment-aware logging (production emits warnings with guidance and license URL).
    • Removed grace-period/evaluation flows and startup-failing license behavior.
  • Documentation

    • README and license setup updated; CI guide removed.
  • Tests

    • Tests updated for status outcomes, caching, reset, thread-safety and logging.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 28, 2026

Caution

Review failed

The head commit changed during the review from baab983 to c714302.

Walkthrough

Converts exception/exit-based license validation to a status-driven model across Node and Ruby: validators now return statuses ('valid' | 'expired' | 'invalid' | 'missing') and log info/warnings instead of raising or exiting; startup initializers, utils, tests, and docs updated to the new status API and production-only paid-license messaging.

Changes

Cohort / File(s) Summary
Node renderer: license status API
packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts, packages/react-on-rails-pro-node-renderer/src/master.ts, packages/react-on-rails-pro-node-renderer/tests/licenseValidator.test.ts
Replaces data-returning API (getValidatedLicenseData/isEvaluation/grace) with getLicenseStatus(): LicenseStatus (`'valid'
Ruby license validator & utils
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb, react_on_rails_pro/lib/react_on_rails_pro/utils.rb, react_on_rails/lib/react_on_rails/utils.rb
Replaces validated_license_data! with license_status and thread-safe @license_status cache; adds determine/load/decode/check helpers, non-throwing status returns, reset! for tests, and updated pro_attribution logic.
Engine initializer
react_on_rails_pro/lib/react_on_rails_pro/engine.rb
Replaces failing initializer with check_license initializer that reads LicenseValidator.license_status and logs case-based messages for :valid, :missing, :expired, :invalid without aborting boot; adds LICENSE_URL and helper logging.
Tests
react_on_rails_pro/spec/..., packages/.../tests/licenseValidator.test.ts
Tests updated to expect symbolic statuses and info/warn logging; removed expectations for exceptions/process.exit and grace/evaluation specifics; added plan and concurrency scenarios; reset behavior adapted to cached status model.
Docs / changelog
react_on_rails_pro/README.md, react_on_rails_pro/LICENSE_SETUP.md, react_on_rails_pro/CHANGELOG.md, react_on_rails_pro/CI_SETUP.md
Switches to a License-Optional Model: no license required for development/CI, paid license required for production; removes CI_SETUP.md and updates README, LICENSE_SETUP, and changelog to reflect production-only paid-license messaging.
Workspace config & sigs
knip.ts, react_on_rails_pro/sig/react_on_rails_pro/license_validator.rbs
Adds node-renderer entry for src/shared/licenseValidator.ts and introduces RBS signature for new license_status and reset! APIs.

Sequence Diagram(s)

sequenceDiagram
  participant App as Application / Rails boot
  participant Engine as ReactOnRailsPro::Engine
  participant License as LicenseValidator
  participant Node as Node renderer (master.ts)
  participant Logger as Logger

  App->>Engine: after_initialize / check_license
  Engine->>License: license_status
  License->>License: load_license_string -> decode_license -> check_plan -> check_expiration
  License-->>Engine: :valid / :expired / :invalid / :missing
  Engine->>Logger: log info or warn message
  Note over Node,License: Node master.ts calls getLicenseStatus() and logs similarly
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • ihabadham
  • alexeyr-ci2

Poem

🐰✨ A Rabbit's Note on Licenses
I hopped through code where exits used to choke,
Now statuses whisper so the app won't choke.
Warnings for production, soft logs for play,
Tests and docs aligned — I dance away! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving from a license-required model to license-optional with HTML attribution comments instead of crashes.

✏️ 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 abanoub/2323-license-optional-attribution

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
Copy Markdown
Contributor

github-actions Bot commented Jan 28, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.5 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.5 KB (0%)
react-on-rails/client bundled (brotli) 53.7 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.7 KB (0%)
react-on-rails-pro/client bundled (gzip) 63.48 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 63.41 KB (-0.12% 🔽)
react-on-rails-pro/client bundled (brotli) 54.58 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.58 KB (0%)
registerServerComponent/client bundled (gzip) 127.38 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.38 KB (0%)
registerServerComponent/client bundled (brotli) 61.58 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.58 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 121.96 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 121.96 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.66 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.66 KB (0%)

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 28, 2026

Pull Request Review

This PR implements a significant architectural change to the React on Rails Pro licensing model, moving from a "license-required" approach (app crashes without valid license) to a "license-optional with attribution" approach (app always runs, shows HTML comment indicating license status).


✅ Strengths

1. Excellent Architectural Simplification

  • The new status-based API (license_status, licensed?, license_data) is much cleaner than the previous exception-throwing model
  • Consistent implementation across Ruby and TypeScript with identical status values: :valid, :expired, :invalid, :missing
  • Non-blocking validation aligns with modern freemium software practices

2. Strong Security Practices

  • JWT signature verification remains strict (verify_expiration: false with manual expiration checking is correct)
  • Algorithm whitelist remains secure (RS256 only) with good comments explaining risks
  • Public key verification is never skipped (critical security requirement)

3. Comprehensive Test Coverage

  • Test coverage is thorough for all license states in both Ruby and TypeScript
  • Tests properly verify caching behavior
  • Good use of mocking to isolate license validation logic

4. Graceful Degradation

  • HTML attribution comments provide visibility without being intrusive
  • Warning logs instead of crashes maintain operational continuity
  • Clear messaging guides users toward licensing options

⚠️ Issues & Concerns

1. CRITICAL: Missing Trailing Newlines (CI Will Fail)

Per your CLAUDE.md requirements, files must end with trailing newlines. Please run:

bundle exec rubocop --auto-correct

2. Security: Error Message Information Disclosure

Exposing error.message in logs could reveal internal implementation details (JWT library versions, parsing logic). Consider using generic error messages instead:

log_license_warning("Invalid license signature. Running in unlicensed mode.")

3. Inconsistent Error Handling Between Ruby and TypeScript

Ruby catches both JWT::DecodeError and StandardError separately. TypeScript only catches Error instances. Consider making them consistent.

4. Potential Race Condition in Caching

Both Ruby and TypeScript cache validation results at module level without thread safety. Ruby should use ||= for atomic assignment or consider a Mutex. TypeScript should document that this runs during single-threaded startup only.

5. Changed Public API - Migration Path Not Clear

The old API (validated_license_data!) has been removed. Consider:

  • Deprecation warnings instead of immediate removal
  • Migration guide in CHANGELOG_PRO.md
  • Version bump guidance (major version bump recommended)

📝 Documentation Gaps

1. CHANGELOG_PRO.md Entry Required

This is a major behavioral change and needs a detailed changelog entry documenting:

  • Breaking change: License no longer required to run
  • Removed methods: validated_license_data!, grace period logic
  • Added methods: license_status, licensed?, license_data

2. Migration Guide Needed

Create documentation explaining:

  • How existing Pro users should update their code
  • What happens to existing license files
  • Version bump implications

✅ Pre-Merge Checklist

  • Run bundle exec rubocop and fix ALL violations
  • Verify all files end with trailing newlines
  • Run full test suite: rake run_rspec:gem && rake run_rspec:dummy
  • Update CHANGELOG_PRO.md with breaking change notice
  • Address security concerns (error message verbosity)
  • Add migration guide for Pro users
  • Consider semantic versioning impact (major version bump?)

🎯 Overall Assessment

Quality: ★★★★☆ (4/5)

This is a well-designed architectural improvement that simplifies the licensing model. The implementation is clean, tests are comprehensive, and security fundamentals are solid.

Main blockers before merge:

  1. Missing trailing newlines (CI will fail)
  2. CHANGELOG_PRO.md update required
  3. Security: Error message verbosity
  4. Full test suite execution

Recommendation: Address the critical issues above, then this is ready to merge. Great work on this PR!

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jan 28, 2026

Greptile Overview

Greptile Summary

This PR changes React on Rails Pro from a “license-required” model to a “license-optional with attribution” model.

On the Ruby side, Pro licensing is now represented as a status (:valid, :expired, :invalid, :missing) computed by ReactOnRailsPro::LicenseValidator with mutex-protected caching, and startup no longer crashes on missing/invalid licenses. ReactOnRailsPro::Engine logs environment-aware messages (warn in production; info elsewhere), and ReactOnRailsPro::Utils.pro_attribution_comment generates an HTML comment reflecting the current license status.

On the Node renderer side, license validation is similarly status-based (getLicenseStatus) with cached evaluation and updated startup logging in the master process. Tests and documentation were updated to match the new behavior and API surface, and Knip configuration was adjusted accordingly.

Confidence Score: 3/5

  • This PR is reasonably safe to merge, but there are a couple of concrete correctness issues that should be addressed first.
  • Most changes are straightforward refactors plus doc/test updates, and the new status-based flow is well covered by specs. However, there is a real runtime compatibility risk in the Ruby validator due to using ActiveSupport-only present? without requiring it, and a small but real semantic bug in the TS expiration check for falsy exp values.
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb; packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts

Important Files Changed

Filename Overview
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb Reworks validator to return status with mutex-cached result; bug: uses ActiveSupport present? without requiring it, which can raise in non-Rails/early-load contexts.
react_on_rails_pro/lib/react_on_rails_pro/engine.rb Logs license status on startup with env-aware messaging; minor wording mismatch for non-prod environments.
packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts Implements status-based license checks with cached result; bug: falsy exp (e.g., 0) is treated as missing due to if (!license.exp).
packages/react-on-rails-pro-node-renderer/src/master.ts Master process now logs license status at startup using new API; behavior change appears consistent.
packages/react-on-rails-pro-node-renderer/tests/licenseValidator.test.ts Updates tests to cover new getLicenseStatus/reset APIs and plan/expiration cases; looks consistent with implementation.
react_on_rails_pro/lib/react_on_rails_pro/utils.rb Adds pro_attribution_comment based on license status; logic is straightforward and covered by specs.
react_on_rails/lib/react_on_rails/utils.rb Adjusts Pro detection to check gem presence only (no license); change aligns with new licensing model.
react_on_rails_pro/sig/react_on_rails_pro/license_validator.rbs Updates RBS signatures for new status-based API; matches Ruby implementation shape.
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb Replaces licensed?/license_data specs with license_status coverage and thread-safety test; seems consistent.
react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb Adds integration tests for env-aware logging behavior; matches engine implementation.
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb Updates utils specs to assert new attribution comment variants; appears correct.
knip.ts Updates Knip entries to treat licenseValidator.ts as a production entry point; no functional risk.
docs/building-features/how-to-conditionally-server-render-based-on-device-type.md Doc link update only; no code impact.
CHANGELOG.md Fixes configuration docs link in changelog entry; no code impact.
react_on_rails_pro/CHANGELOG.md Documents licensing model shift and plan validation; no code impact.
react_on_rails_pro/LICENSE_SETUP.md Updates license setup docs for license-optional model; no code impact.
react_on_rails_pro/README.md Updates Pro README to reflect license-optional model and new docs; no code impact.
react_on_rails_pro/CI_SETUP.md PR indicates CI_SETUP is removed, but file is listed as changed; verify it’s actually deleted/updated as expected (file not found in checkout).

Sequence Diagram

sequenceDiagram
  autonumber
  participant App as Rails app boot
  participant Engine as ReactOnRailsPro::Engine
  participant LV as ReactOnRailsPro::LicenseValidator
  participant Utils as ReactOnRailsPro::Utils
  participant View as Rails view render

  App->>Engine: initializer "check_license"
  Engine->>LV: license_status()
  LV-->>Engine: :valid | :missing | :expired | :invalid
  Engine->>Engine: log_license_issue / info

  View->>Utils: pro_attribution_comment()
  Utils->>LV: license_status()
  LV-->>Utils: status symbol
  Utils-->>View: "<!-- Powered by ... | STATUS -->"
Loading

Copy link
Copy Markdown
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: 3

🤖 Fix all issues with AI agents
In `@packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts`:
- Around line 105-126: Update the JSDoc for the checkExpiration function to
accurately list all possible return values: 'valid', 'expired', and 'invalid'
(used when the license.exp field is missing). Modify the `@returns` description on
the checkExpiration function to mention each status and the condition that
produces it (e.g., 'invalid' when exp is absent, 'expired' when exp < now,
'valid' otherwise), so the doc correctly reflects the implementation.

In `@react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb`:
- Around line 29-37: The method license_data currently caches and returns
decoded license info even when license_status is :expired or :invalid; change it
so decoded data is only cached/returned when license_status == :valid: call
license_status, and only set/return `@license_data` if license_status equals
:valid (otherwise ensure `@license_data` is nil), and mirror this behavior at the
other caching site referenced (the similar cache assignments around lines 64-66)
so callers never receive decoded data for non-valid licenses; use the existing
license_status and `@license_data` symbols to locate and adjust the logic.
- Around line 73-83: The load_license_string method currently calls
File.read(config_path).strip without guarding against IO/encoding/permission
errors; update load_license_string to rescue standard file-read exceptions
(e.g., Errno::EACCES, Errno::ENOENT, Encoding::InvalidByteSequenceError,
IOError, and StandardError as a fallback) when reading config_path, log a
warning via Rails.logger.warn (or process logger) that the license file could
not be read including the error message, and return nil so the method maintains
its "never raises" contract; keep the existing ENV.fetch behavior and only apply
the rescue around the File.read(config_path).strip call.
🧹 Nitpick comments (1)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)

59-63: Consider using a more robust caching verification approach.

Testing caching by expecting determine_license_status (a private method) to not be called couples the test to implementation details. If the internal method name changes, this test will break even though the behavior is correct.

A more robust approach would verify caching via observable behavior:

♻️ Suggested alternative for verifying caching
      it "caches the result" do
-       described_class.license_status
-       expect(described_class).not_to receive(:determine_license_status)
-       described_class.license_status
+       # Verify same object is returned (cached)
+       first_call = described_class.license_status
+       second_call = described_class.license_status
+       expect(first_call).to equal(second_call)
      end

Comment thread react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 28, 2026

Code Review: License Requirement Removal

I've reviewed PR #2324 and overall this is an excellent refactoring. The architectural shift from "fail-fast" to "warn-and-continue" is well-executed across both Ruby and TypeScript codebases. Here's my detailed feedback:


✅ Strengths

1. Excellent API Design

The new status-based API (:valid, :expired, :invalid, :missing) is much cleaner than the previous exception-based approach:

# Before: Exception-based (forces error handling everywhere)
def validated_license_data\!
  # raises on error
end

# After: Status-based (allows graceful degradation)
def license_status
  # returns symbol, never raises
end

This makes the code much easier to reason about and test.

2. Consistent Implementation Across Languages

Both Ruby and TypeScript implementations follow the same three-step pattern:

  1. Load license string
  2. Decode and verify JWT
  3. Check expiration

The parallel structure makes maintenance easier and reduces cognitive load.

3. Comprehensive Test Coverage

The test changes are thorough:

  • All license states tested (:valid, :expired, :invalid, :missing)
  • Proper test isolation with reset\! in setup
  • Removed obsolete grace period tests (no longer relevant)

4. Security Maintained

JWT verification remains secure:

  • algorithm: "RS256" enforced (prevents downgrade attacks)
  • Signature verification always enabled
  • Manual expiration handling (allows for custom logic)

⚠️ Issues to Address

1. CRITICAL: Inconsistent License Detection in Open Source Code

Location: react_on_rails/lib/react_on_rails/utils.rb:259-263

The react_on_rails_pro? method only checks if the Pro gem is installed, not if it's licensed:

def self.react_on_rails_pro?
  @react_on_rails_pro = gem_available?("react_on_rails_pro")
end

Problem: This means that users with expired/invalid/missing licenses will still get Pro-like behavior (e.g., immediate hydration defaults to true when nil).

Impact: The open-source code at utils.rb:51 uses this method:

# If nil, default based on Pro license status
return react_on_rails_pro? if value.nil?

This will return true even for unlicensed Pro installations.

Recommendation: Update react_on_rails_pro? to check both gem presence AND license validity:

def self.react_on_rails_pro?
  return @react_on_rails_pro if defined?(@react_on_rails_pro)
  
  @react_on_rails_pro = gem_available?("react_on_rails_pro") && 
                        ReactOnRailsPro::Utils.licensed?
end

Alternative: If you want to maintain backwards compatibility, create a new method:

def self.react_on_rails_pro_licensed?
  react_on_rails_pro? && ReactOnRailsPro::Utils.licensed?
end

Then update all feature checks to use react_on_rails_pro_licensed? instead.


2. Missing Error Handling in License Data Caching

Location: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb:65

# Cache the license data if we got this far
@license_data = decoded_data

Issue: If check_expiration returns :expired or :invalid, you're still caching the license data. This is fine, but the license_data method doesn't document this behavior.

Recommendation: Update the documentation for license_data to clarify:

# Returns license data if available (never raises)
# Note: Returns data even for expired/invalid licenses (status check is separate)
# @return [Hash, nil] License data or nil if not available
def license_data

This makes it clear that license_data and license_status are independent checks.


3. TypeScript Type Safety: Missing Return Type

Location: packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts:133

function determineLicenseStatus(): LicenseStatus {
  // ...
  logLicenseInfo(decodedData);
  
  status
  // ^^^ Missing 'return' keyword\!
}

Issue: Line 149 in the new code has status without return. This should be return status;

Impact: TypeScript compiler will catch this, but it will cause runtime errors.

Fix: Change line 149 to:

return status;

4. Potential Race Condition in Cache Initialization

Location: Both license_validator.rb and licenseValidator.ts

def license_status
  return @license_status if defined?(@license_status)
  
  @license_status = determine_license_status  # Not thread-safe
end

Issue: If multiple threads call license_status simultaneously during initialization, determine_license_status could run multiple times.

Impact:

  • Multiple log messages
  • Slight performance hit
  • No data corruption (idempotent operations)

Recommendation: Not critical for Rails (single-threaded request handling), but consider adding thread-safety if Node renderer uses worker threads:

def license_status
  @mutex ||= Mutex.new
  return @license_status if defined?(@license_status)
  
  @mutex.synchronize do
    return @license_status if defined?(@license_status)
    @license_status = determine_license_status
  end
end

5. Documentation: Missing Migration Guide

Issue: The PR changes public API significantly:

  • validated_license_data\!license_status + license_data
  • grace_days_remaining removed
  • evaluation? removed

Recommendation: Add a migration section to the PR description:

## Migration Guide (for internal code)

### If you were calling:
- `validated_license_data\!` → Use `license_status` to check, `license_data` for data
- `grace_days_remaining` → No longer applicable (no grace period)
- `evaluation?` → Check `license_data["plan"]` directly

### Breaking Changes:
- No longer raises exceptions on invalid license
- App continues running regardless of license state

🔍 Code Quality Observations

Good Practices:

  1. ✅ Proper use of frozen_string_literal: true
  2. ✅ Comprehensive RSpec tests with proper mocking
  3. ✅ Security-conscious JWT verification
  4. ✅ Clear separation of concerns (load → decode → validate)
  5. ✅ Consistent logging levels (info for success, warn for problems)

Minor Suggestions:

  1. Naming: Consider renaming licensed? to valid_license? for clarity
  2. Constants: The VALID_STATUSES constant in Ruby is defined but never used - consider removing or documenting its purpose
  3. TypeScript: The LicenseData interface has exp: number as required, but checkExpiration checks if it exists. Consider making it optional in the type: exp?: number

🎯 Performance Considerations

Positive:

  • ✅ Proper caching of license status (O(1) after first call)
  • ✅ No repeated file I/O after initial load
  • ✅ Early returns in validation chain

No concerns about performance impact from this change.


🔒 Security Assessment

Strengths:

  1. ✅ JWT signature always verified (verify: true)
  2. ✅ Algorithm locked to RS256 (prevents downgrade attacks)
  3. ✅ No secrets in environment by design (public key verification)

No security regressions detected.


📋 Test Plan Verification

The PR description lists test scenarios - all are covered by the RSpec tests:

  • ✅ App starts without license (returns :missing)
  • ✅ App starts with expired license (returns :expired)
  • ✅ App starts with valid license (returns :valid)
  • ✅ Invalid license handling (returns :invalid)

Recommendation: Add one more test case:

context "when license file is corrupted (not valid JWT)" do
  before do
    ENV["REACT_ON_RAILS_PRO_LICENSE"] = "not-a-valid-jwt-token"
  end
  
  it "returns :invalid" do
    expect(described_class.license_status).to eq(:invalid)
  end
end

🚀 Summary

Overall Assessment: ⭐⭐⭐⭐ (4/5 stars)

This is a well-designed refactoring that successfully achieves the goal of making the license optional. The code is clean, well-tested, and maintains security.

Must Fix Before Merge:

  1. 🔴 CRITICAL: Fix react_on_rails_pro? to check license validity (utils.rb:259)
  2. 🔴 CRITICAL: Add missing return in TypeScript determineLicenseStatus (line ~149)

Should Fix:
3. 🟡 Update license_data documentation to clarify behavior with expired/invalid licenses
4. 🟡 Add migration guide to PR description

Nice to Have:
5. 🟢 Consider thread-safety for Node renderer
6. 🟢 Add corrupted JWT test case
7. 🟢 Remove unused VALID_STATUSES constant or document its purpose


Great work on this refactoring! The architectural shift from exceptions to status codes is exactly the right approach for this use case. Once the critical issues are addressed, this will be ready to merge.

Let me know if you'd like me to elaborate on any of these points!

@github-actions
Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchabanoub/2323-license-optional-attribution
Testbedgithub-actions
Click to view all benchmark results
Benchmarkfailed_pctBenchmark Result
%
(Result Δ%)
Upper Boundary
%
(Limit %)
p50_latencyBenchmark Result
ms
(Result Δ%)
Upper Boundary
ms
(Limit %)
p90_latencyBenchmark Result
ms
(Result Δ%)
Upper Boundary
ms
(Limit %)
p99_latencyBenchmark Result
ms
(Result Δ%)
Upper Boundary
ms
(Limit %)
rpsBenchmark Result
req/s
(Result Δ%)
Lower Boundary
req/s
(Limit %)
/: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
3,167.10
(0.00%)Baseline: 3,167.10
4,117.23
(76.92%)
📈 view plot
🚷 view threshold
3,851.36
(0.00%)Baseline: 3,851.36
5,006.77
(76.92%)
📈 view plot
🚷 view threshold
33,082.55
(0.00%)Baseline: 33,082.55
43,007.32
(76.92%)
📈 view plot
🚷 view threshold
2.58
(0.00%)Baseline: 2.58
1.81
(70.00%)
/: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
33.88
(0.00%)Baseline: 33.88
44.04
(76.92%)
📈 view plot
🚷 view threshold
55.60
(0.00%)Baseline: 55.60
72.28
(76.92%)
📈 view plot
🚷 view threshold
380.20
(0.00%)Baseline: 380.20
494.26
(76.92%)
📈 view plot
🚷 view threshold
217.36
(0.00%)Baseline: 217.36
152.15
(70.00%)
/apollo_graphql: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
43.01
(0.00%)Baseline: 43.01
55.91
(76.92%)
📈 view plot
🚷 view threshold
117.75
(0.00%)Baseline: 117.75
153.07
(76.92%)
📈 view plot
🚷 view threshold
494.28
(0.00%)Baseline: 494.28
642.56
(76.92%)
📈 view plot
🚷 view threshold
130.97
(0.00%)Baseline: 130.97
91.68
(70.00%)
/async_components_demo: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
32.23
(0.00%)Baseline: 32.23
41.90
(76.92%)
📈 view plot
🚷 view threshold
44.62
(0.00%)Baseline: 44.62
58.01
(76.92%)
📈 view plot
🚷 view threshold
372.87
(0.00%)Baseline: 372.87
484.73
(76.92%)
📈 view plot
🚷 view threshold
183.80
(0.00%)Baseline: 183.80
128.66
(70.00%)
/async_on_server_sync_on_client: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
19.80
(0.00%)Baseline: 19.80
25.74
(76.92%)
📈 view plot
🚷 view threshold
32.04
(0.00%)Baseline: 32.04
41.65
(76.92%)
📈 view plot
🚷 view threshold
277.57
(0.00%)Baseline: 277.57
360.84
(76.92%)
📈 view plot
🚷 view threshold
379.49
(0.00%)Baseline: 379.49
265.64
(70.00%)
/async_on_server_sync_on_client_client_render: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.12
(0.00%)Baseline: 17.12
22.26
(76.92%)
📈 view plot
🚷 view threshold
25.65
(0.00%)Baseline: 25.65
33.34
(76.92%)
📈 view plot
🚷 view threshold
189.01
(0.00%)Baseline: 189.01
245.71
(76.92%)
📈 view plot
🚷 view threshold
345.02
(0.00%)Baseline: 345.02
241.51
(70.00%)
/async_render_function_returns_component: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
19.79
(0.00%)Baseline: 19.79
25.73
(76.92%)
📈 view plot
🚷 view threshold
26.72
(0.00%)Baseline: 26.72
34.74
(76.92%)
📈 view plot
🚷 view threshold
227.43
(0.00%)Baseline: 227.43
295.66
(76.92%)
📈 view plot
🚷 view threshold
299.31
(0.00%)Baseline: 299.31
209.52
(70.00%)
/async_render_function_returns_string: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
19.77
(0.00%)Baseline: 19.77
25.70
(76.92%)
📈 view plot
🚷 view threshold
26.56
(0.00%)Baseline: 26.56
34.53
(76.92%)
📈 view plot
🚷 view threshold
227.11
(0.00%)Baseline: 227.11
295.24
(76.92%)
📈 view plot
🚷 view threshold
299.99
(0.00%)Baseline: 299.99
209.99
(70.00%)
/broken_app: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
145.57
(0.00%)Baseline: 145.57
189.24
(76.92%)
📈 view plot
🚷 view threshold
424.22
(0.00%)Baseline: 424.22
551.49
(76.92%)
📈 view plot
🚷 view threshold
7,873.03
(0.00%)Baseline: 7,873.03
10,234.94
(76.92%)
📈 view plot
🚷 view threshold
23.14
(0.00%)Baseline: 23.14
16.20
(70.00%)
/broken_app: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.72
(0.00%)Baseline: 17.72
23.04
(76.92%)
📈 view plot
🚷 view threshold
43.84
(0.00%)Baseline: 43.84
56.99
(76.92%)
📈 view plot
🚷 view threshold
186.63
(0.00%)Baseline: 186.63
242.62
(76.92%)
📈 view plot
🚷 view threshold
333.40
(0.00%)Baseline: 333.40
233.38
(70.00%)
/cached_react_helmet: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.06
(0.00%)Baseline: 17.06
22.18
(76.92%)
📈 view plot
🚷 view threshold
28.10
(0.00%)Baseline: 28.10
36.53
(76.92%)
📈 view plot
🚷 view threshold
197.63
(0.00%)Baseline: 197.63
256.92
(76.92%)
📈 view plot
🚷 view threshold
443.48
(0.00%)Baseline: 443.48
310.44
(70.00%)
/cached_redux_component: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.95
(0.00%)Baseline: 17.95
23.34
(76.92%)
📈 view plot
🚷 view threshold
30.05
(0.00%)Baseline: 30.05
39.06
(76.92%)
📈 view plot
🚷 view threshold
214.04
(0.00%)Baseline: 214.04
278.25
(76.92%)
📈 view plot
🚷 view threshold
420.05
(0.00%)Baseline: 420.05
294.04
(70.00%)
/client_side_hello_world: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
11.92
(0.00%)Baseline: 11.92
15.50
(76.92%)
📈 view plot
🚷 view threshold
15.53
(0.00%)Baseline: 15.53
20.19
(76.92%)
📈 view plot
🚷 view threshold
137.83
(0.00%)Baseline: 137.83
179.18
(76.92%)
📈 view plot
🚷 view threshold
496.10
(0.00%)Baseline: 496.10
347.27
(70.00%)
/client_side_hello_world: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.85
(0.00%)Baseline: 17.85
23.20
(76.92%)
📈 view plot
🚷 view threshold
28.62
(0.00%)Baseline: 28.62
37.21
(76.92%)
📈 view plot
🚷 view threshold
203.09
(0.00%)Baseline: 203.09
264.02
(76.92%)
📈 view plot
🚷 view threshold
437.21
(0.00%)Baseline: 437.21
306.05
(70.00%)
/client_side_hello_world_shared_store: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
8.53
(0.00%)Baseline: 8.53
11.09
(76.92%)
📈 view plot
🚷 view threshold
16.35
(0.00%)Baseline: 16.35
21.26
(76.92%)
📈 view plot
🚷 view threshold
203.42
(0.00%)Baseline: 203.42
264.45
(76.92%)
📈 view plot
🚷 view threshold
646.94
(0.00%)Baseline: 646.94
452.86
(70.00%)
/client_side_hello_world_shared_store: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.93
(0.00%)Baseline: 17.93
23.31
(76.92%)
📈 view plot
🚷 view threshold
21.47
(0.00%)Baseline: 21.47
27.91
(76.92%)
📈 view plot
🚷 view threshold
361.60
(0.00%)Baseline: 361.60
470.08
(76.92%)
📈 view plot
🚷 view threshold
325.45
(0.00%)Baseline: 325.45
227.81
(70.00%)
/client_side_hello_world_shared_store_controller: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
9.73
(0.00%)Baseline: 9.73
12.65
(76.92%)
📈 view plot
🚷 view threshold
18.83
(0.00%)Baseline: 18.83
24.48
(76.92%)
📈 view plot
🚷 view threshold
150.97
(0.00%)Baseline: 150.97
196.26
(76.92%)
📈 view plot
🚷 view threshold
676.65
(0.00%)Baseline: 676.65
473.65
(70.00%)
/client_side_hello_world_shared_store_controller: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.96
(0.00%)Baseline: 17.96
23.35
(76.92%)
📈 view plot
🚷 view threshold
28.81
(0.00%)Baseline: 28.81
37.45
(76.92%)
📈 view plot
🚷 view threshold
203.68
(0.00%)Baseline: 203.68
264.78
(76.92%)
📈 view plot
🚷 view threshold
436.51
(0.00%)Baseline: 436.51
305.56
(70.00%)
/client_side_hello_world_shared_store_defer: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
9.65
(0.00%)Baseline: 9.65
12.54
(76.92%)
📈 view plot
🚷 view threshold
19.11
(0.00%)Baseline: 19.11
24.84
(76.92%)
📈 view plot
🚷 view threshold
112.85
(0.00%)Baseline: 112.85
146.70
(76.92%)
📈 view plot
🚷 view threshold
697.64
(0.00%)Baseline: 697.64
488.35
(70.00%)
/client_side_hello_world_shared_store_defer: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
14.27
(0.00%)Baseline: 14.27
18.55
(76.92%)
📈 view plot
🚷 view threshold
23.76
(0.00%)Baseline: 23.76
30.89
(76.92%)
📈 view plot
🚷 view threshold
234.52
(0.00%)Baseline: 234.52
304.88
(76.92%)
📈 view plot
🚷 view threshold
441.06
(0.00%)Baseline: 441.06
308.74
(70.00%)
/client_side_log_throw: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
7.49
(0.00%)Baseline: 7.49
9.74
(76.92%)
📈 view plot
🚷 view threshold
13.04
(0.00%)Baseline: 13.04
16.95
(76.92%)
📈 view plot
🚷 view threshold
165.73
(0.00%)Baseline: 165.73
215.45
(76.92%)
📈 view plot
🚷 view threshold
779.41
(0.00%)Baseline: 779.41
545.59
(70.00%)
/client_side_log_throw: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
15.81
(0.00%)Baseline: 15.81
20.55
(76.92%)
📈 view plot
🚷 view threshold
38.97
(0.00%)Baseline: 38.97
50.66
(76.92%)
📈 view plot
🚷 view threshold
171.46
(0.00%)Baseline: 171.46
222.90
(76.92%)
📈 view plot
🚷 view threshold
370.84
(0.00%)Baseline: 370.84
259.59
(70.00%)
/client_side_manual_render: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
11.03
(0.00%)Baseline: 11.03
14.34
(76.92%)
📈 view plot
🚷 view threshold
18.53
(0.00%)Baseline: 18.53
24.09
(76.92%)
📈 view plot
🚷 view threshold
59.55
(0.00%)Baseline: 59.55
77.41
(76.92%)
📈 view plot
🚷 view threshold
769.13
(0.00%)Baseline: 769.13
538.39
(70.00%)
/client_side_manual_render: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.69
(0.00%)Baseline: 17.69
23.00
(76.92%)
📈 view plot
🚷 view threshold
27.90
(0.00%)Baseline: 27.90
36.27
(76.92%)
📈 view plot
🚷 view threshold
194.80
(0.00%)Baseline: 194.80
253.24
(76.92%)
📈 view plot
🚷 view threshold
444.94
(0.00%)Baseline: 444.94
311.46
(70.00%)
/client_side_rescript_hello_world: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
11.31
(0.00%)Baseline: 11.31
14.70
(76.92%)
📈 view plot
🚷 view threshold
19.42
(0.00%)Baseline: 19.42
25.25
(76.92%)
📈 view plot
🚷 view threshold
67.04
(0.00%)Baseline: 67.04
87.15
(76.92%)
📈 view plot
🚷 view threshold
735.83
(0.00%)Baseline: 735.83
515.08
(70.00%)
/console_logs_in_async_server: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
2,117.64
(0.00%)Baseline: 2,117.64
2,752.93
(76.92%)
📈 view plot
🚷 view threshold
2,182.71
(0.00%)Baseline: 2,182.71
2,837.52
(76.92%)
📈 view plot
🚷 view threshold
23,404.80
(0.00%)Baseline: 23,404.80
30,426.24
(76.92%)
📈 view plot
🚷 view threshold
2.75
(0.00%)Baseline: 2.75
1.93
(70.00%)
/context_function_return_jsx: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
7.80
(0.00%)Baseline: 7.80
10.14
(76.92%)
📈 view plot
🚷 view threshold
13.01
(0.00%)Baseline: 13.01
16.91
(76.92%)
📈 view plot
🚷 view threshold
140.35
(0.00%)Baseline: 140.35
182.46
(76.92%)
📈 view plot
🚷 view threshold
787.63
(0.00%)Baseline: 787.63
551.34
(70.00%)
/context_function_return_jsx: Pro📈 view plot
🚷 view threshold
100.00
(0.00%)Baseline: 100.00
130.00
(76.92%)
📈 view plot
🚷 view threshold
12.81
(0.00%)Baseline: 12.81
16.65
(76.92%)
📈 view plot
🚷 view threshold
21.23
(0.00%)Baseline: 21.23
27.60
(76.92%)
📈 view plot
🚷 view threshold
200.77
(0.00%)Baseline: 200.77
261.00
(76.92%)
📈 view plot
🚷 view threshold
501.11
(0.00%)Baseline: 501.11
350.78
(70.00%)
/css_modules_images_fonts_example: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
348.06
(0.00%)Baseline: 348.06
452.48
(76.92%)
📈 view plot
🚷 view threshold
434.02
(0.00%)Baseline: 434.02
564.23
(76.92%)
📈 view plot
🚷 view threshold
4,205.82
(0.00%)Baseline: 4,205.82
5,467.57
(76.92%)
📈 view plot
🚷 view threshold
23.32
(0.00%)Baseline: 23.32
16.32
(70.00%)
/css_modules_images_fonts_example: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.99
(0.00%)Baseline: 17.99
23.39
(76.92%)
📈 view plot
🚷 view threshold
27.68
(0.00%)Baseline: 27.68
35.98
(76.92%)
📈 view plot
🚷 view threshold
248.56
(0.00%)Baseline: 248.56
323.13
(76.92%)
📈 view plot
🚷 view threshold
428.46
(0.00%)Baseline: 428.46
299.92
(70.00%)
/empty: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
5.35
(0.00%)Baseline: 5.35
6.95
(76.92%)
📈 view plot
🚷 view threshold
8.25
(0.00%)Baseline: 8.25
10.72
(76.92%)
📈 view plot
🚷 view threshold
57.69
(0.00%)Baseline: 57.69
75.00
(76.92%)
📈 view plot
🚷 view threshold
1,486.08
(0.00%)Baseline: 1,486.08
1,040.26
(70.00%)
/image_example: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
343.49
(0.00%)Baseline: 343.49
446.54
(76.92%)
📈 view plot
🚷 view threshold
432.51
(0.00%)Baseline: 432.51
562.26
(76.92%)
📈 view plot
🚷 view threshold
6,235.32
(0.00%)Baseline: 6,235.32
8,105.92
(76.92%)
📈 view plot
🚷 view threshold
17.23
(0.00%)Baseline: 17.23
12.06
(70.00%)
/image_example: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
18.20
(0.00%)Baseline: 18.20
23.66
(76.92%)
📈 view plot
🚷 view threshold
46.69
(0.00%)Baseline: 46.69
60.70
(76.92%)
📈 view plot
🚷 view threshold
192.42
(0.00%)Baseline: 192.42
250.15
(76.92%)
📈 view plot
🚷 view threshold
323.52
(0.00%)Baseline: 323.52
226.46
(70.00%)
/lazy_apollo_graphql: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
51.14
(0.00%)Baseline: 51.14
66.48
(76.92%)
📈 view plot
🚷 view threshold
79.83
(0.00%)Baseline: 79.83
103.78
(76.92%)
📈 view plot
🚷 view threshold
549.91
(0.00%)Baseline: 549.91
714.88
(76.92%)
📈 view plot
🚷 view threshold
151.63
(0.00%)Baseline: 151.63
106.14
(70.00%)
/loadable: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
23.24
(0.00%)Baseline: 23.24
30.21
(76.92%)
📈 view plot
🚷 view threshold
33.61
(0.00%)Baseline: 33.61
43.69
(76.92%)
📈 view plot
🚷 view threshold
136.16
(0.00%)Baseline: 136.16
177.01
(76.92%)
📈 view plot
🚷 view threshold
361.07
(0.00%)Baseline: 361.07
252.75
(70.00%)
/manual_render_test: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
9.23
(0.00%)Baseline: 9.23
12.00
(76.92%)
📈 view plot
🚷 view threshold
17.94
(0.00%)Baseline: 17.94
23.32
(76.92%)
📈 view plot
🚷 view threshold
112.70
(0.00%)Baseline: 112.70
146.51
(76.92%)
📈 view plot
🚷 view threshold
727.46
(0.00%)Baseline: 727.46
509.22
(70.00%)
/posts_page: Pro📈 view plot
🚷 view threshold
100.00
(0.00%)Baseline: 100.00
130.00
(76.92%)
📈 view plot
🚷 view threshold
8.11
(0.00%)Baseline: 8.11
10.54
(76.92%)
📈 view plot
🚷 view threshold
12.49
(0.00%)Baseline: 12.49
16.24
(76.92%)
📈 view plot
🚷 view threshold
89.99
(0.00%)Baseline: 89.99
116.99
(76.92%)
📈 view plot
🚷 view threshold
976.98
(0.00%)Baseline: 976.98
683.89
(70.00%)
/pure_component: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
344.02
(0.00%)Baseline: 344.02
447.23
(76.92%)
📈 view plot
🚷 view threshold
426.37
(0.00%)Baseline: 426.37
554.28
(76.92%)
📈 view plot
🚷 view threshold
4,055.42
(0.00%)Baseline: 4,055.42
5,272.05
(76.92%)
📈 view plot
🚷 view threshold
23.56
(0.00%)Baseline: 23.56
16.49
(70.00%)
/pure_component: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
13.07
(0.00%)Baseline: 13.07
16.99
(76.92%)
📈 view plot
🚷 view threshold
24.49
(0.00%)Baseline: 24.49
31.84
(76.92%)
📈 view plot
🚷 view threshold
329.04
(0.00%)Baseline: 329.04
427.75
(76.92%)
📈 view plot
🚷 view threshold
438.33
(0.00%)Baseline: 438.33
306.83
(70.00%)
/pure_component_wrapped_in_function: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
10.90
(0.00%)Baseline: 10.90
14.17
(76.92%)
📈 view plot
🚷 view threshold
13.04
(0.00%)Baseline: 13.04
16.95
(76.92%)
📈 view plot
🚷 view threshold
222.75
(0.00%)Baseline: 222.75
289.57
(76.92%)
📈 view plot
🚷 view threshold
529.66
(0.00%)Baseline: 529.66
370.76
(70.00%)
/pure_component_wrapped_in_function: Pro📈 view plot
🚷 view threshold
100.00
(0.00%)Baseline: 100.00
130.00
(76.92%)
📈 view plot
🚷 view threshold
15.40
(0.00%)Baseline: 15.40
20.02
(76.92%)
📈 view plot
🚷 view threshold
25.75
(0.00%)Baseline: 25.75
33.48
(76.92%)
📈 view plot
🚷 view threshold
173.55
(0.00%)Baseline: 173.55
225.62
(76.92%)
📈 view plot
🚷 view threshold
497.69
(0.00%)Baseline: 497.69
348.38
(70.00%)
/react_helmet: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
346.96
(0.00%)Baseline: 346.96
451.05
(76.92%)
📈 view plot
🚷 view threshold
584.84
(0.00%)Baseline: 584.84
760.29
(76.92%)
📈 view plot
🚷 view threshold
3,792.56
(0.00%)Baseline: 3,792.56
4,930.33
(76.92%)
📈 view plot
🚷 view threshold
17.18
(0.00%)Baseline: 17.18
12.03
(70.00%)
/react_helmet: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
18.77
(0.00%)Baseline: 18.77
24.40
(76.92%)
📈 view plot
🚷 view threshold
28.80
(0.00%)Baseline: 28.80
37.44
(76.92%)
📈 view plot
🚷 view threshold
194.25
(0.00%)Baseline: 194.25
252.53
(76.92%)
📈 view plot
🚷 view threshold
411.81
(0.00%)Baseline: 411.81
288.27
(70.00%)
/react_helmet_broken: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
362.11
(0.00%)Baseline: 362.11
470.74
(76.92%)
📈 view plot
🚷 view threshold
449.08
(0.00%)Baseline: 449.08
583.80
(76.92%)
📈 view plot
🚷 view threshold
4,152.63
(0.00%)Baseline: 4,152.63
5,398.42
(76.92%)
📈 view plot
🚷 view threshold
22.61
(0.00%)Baseline: 22.61
15.83
(70.00%)
/react_helmet_broken: Pro📈 view plot
🚷 view threshold
100.00
(0.00%)Baseline: 100.00
130.00
(76.92%)
📈 view plot
🚷 view threshold
16.38
(0.00%)Baseline: 16.38
21.29
(76.92%)
📈 view plot
🚷 view threshold
19.58
(0.00%)Baseline: 19.58
25.45
(76.92%)
📈 view plot
🚷 view threshold
324.16
(0.00%)Baseline: 324.16
421.41
(76.92%)
📈 view plot
🚷 view threshold
354.79
(0.00%)Baseline: 354.79
248.35
(70.00%)
/react_router: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
366.83
(0.00%)Baseline: 366.83
476.88
(76.92%)
📈 view plot
🚷 view threshold
462.22
(0.00%)Baseline: 462.22
600.89
(76.92%)
📈 view plot
🚷 view threshold
4,163.97
(0.00%)Baseline: 4,163.97
5,413.16
(76.92%)
📈 view plot
🚷 view threshold
22.10
(0.00%)Baseline: 22.10
15.47
(70.00%)
/react_router: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
13.98
(0.00%)Baseline: 13.98
18.17
(76.92%)
📈 view plot
🚷 view threshold
21.55
(0.00%)Baseline: 21.55
28.02
(76.92%)
📈 view plot
🚷 view threshold
193.61
(0.00%)Baseline: 193.61
251.69
(76.92%)
📈 view plot
🚷 view threshold
474.72
(0.00%)Baseline: 474.72
332.30
(70.00%)
/redis_receiver: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
64.11
(0.00%)Baseline: 64.11
83.34
(76.92%)
📈 view plot
🚷 view threshold
106.63
(0.00%)Baseline: 106.63
138.62
(76.92%)
📈 view plot
🚷 view threshold
892.24
(0.00%)Baseline: 892.24
1,159.91
(76.92%)
📈 view plot
🚷 view threshold
104.37
(0.00%)Baseline: 104.37
73.06
(70.00%)
/render_js: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
323.54
(0.00%)Baseline: 323.54
420.60
(76.92%)
📈 view plot
🚷 view threshold
385.99
(0.00%)Baseline: 385.99
501.79
(76.92%)
📈 view plot
🚷 view threshold
6,255.65
(0.00%)Baseline: 6,255.65
8,132.34
(76.92%)
📈 view plot
🚷 view threshold
18.32
(0.00%)Baseline: 18.32
12.82
(70.00%)
/render_js: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.43
(0.00%)Baseline: 17.43
22.66
(76.92%)
📈 view plot
🚷 view threshold
36.98
(0.00%)Baseline: 36.98
48.07
(76.92%)
📈 view plot
🚷 view threshold
192.00
(0.00%)Baseline: 192.00
249.60
(76.92%)
📈 view plot
🚷 view threshold
335.29
(0.00%)Baseline: 335.29
234.70
(70.00%)
/rendered_html: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
397.47
(0.00%)Baseline: 397.47
516.71
(76.92%)
📈 view plot
🚷 view threshold
445.40
(0.00%)Baseline: 445.40
579.02
(76.92%)
📈 view plot
🚷 view threshold
2,875.43
(0.00%)Baseline: 2,875.43
3,738.06
(76.92%)
📈 view plot
🚷 view threshold
22.91
(0.00%)Baseline: 22.91
16.04
(70.00%)
/rendered_html: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.81
(0.00%)Baseline: 17.81
23.15
(76.92%)
📈 view plot
🚷 view threshold
26.86
(0.00%)Baseline: 26.86
34.92
(76.92%)
📈 view plot
🚷 view threshold
243.69
(0.00%)Baseline: 243.69
316.80
(76.92%)
📈 view plot
🚷 view threshold
438.04
(0.00%)Baseline: 438.04
306.63
(70.00%)
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchabanoub/2323-license-optional-attribution
Testbedgithub-actions
Click to view all benchmark results
Benchmarkfailed_pctBenchmark Result
%
(Result Δ%)
Upper Boundary
%
(Limit %)
p50_latencyBenchmark Result
ms
(Result Δ%)
Upper Boundary
ms
(Limit %)
p90_latencyBenchmark Result
ms
(Result Δ%)
Upper Boundary
ms
(Limit %)
p99_latencyBenchmark Result
ms
(Result Δ%)
Upper Boundary
ms
(Limit %)
rpsBenchmark Result
req/s
(Result Δ%)
Lower Boundary
req/s
(Limit %)
/rsc_posts_page_over_http: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
19.45
(0.00%)Baseline: 19.45
25.29
(76.92%)
📈 view plot
🚷 view threshold
32.60
(0.00%)Baseline: 32.60
42.38
(76.92%)
📈 view plot
🚷 view threshold
223.89
(0.00%)Baseline: 223.89
291.06
(76.92%)
📈 view plot
🚷 view threshold
393.76
(0.00%)Baseline: 393.76
275.63
(70.00%)
/rsc_posts_page_over_redis: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
60.25
(0.00%)Baseline: 60.25
78.33
(76.92%)
📈 view plot
🚷 view threshold
92.48
(0.00%)Baseline: 92.48
120.22
(76.92%)
📈 view plot
🚷 view threshold
811.90
(0.00%)Baseline: 811.90
1,055.47
(76.92%)
📈 view plot
🚷 view threshold
113.91
(0.00%)Baseline: 113.91
79.74
(70.00%)
/server_render_with_timeout: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
15.03
(0.00%)Baseline: 15.03
19.54
(76.92%)
📈 view plot
🚷 view threshold
24.55
(0.00%)Baseline: 24.55
31.91
(76.92%)
📈 view plot
🚷 view threshold
236.53
(0.00%)Baseline: 236.53
307.49
(76.92%)
📈 view plot
🚷 view threshold
428.02
(0.00%)Baseline: 428.02
299.61
(70.00%)
/server_router: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
19.78
(0.00%)Baseline: 19.78
25.71
(76.92%)
📈 view plot
🚷 view threshold
32.13
(0.00%)Baseline: 32.13
41.77
(76.92%)
📈 view plot
🚷 view threshold
279.06
(0.00%)Baseline: 279.06
362.78
(76.92%)
📈 view plot
🚷 view threshold
376.67
(0.00%)Baseline: 376.67
263.67
(70.00%)
/server_router_client_render: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
18.53
(0.00%)Baseline: 18.53
24.09
(76.92%)
📈 view plot
🚷 view threshold
23.88
(0.00%)Baseline: 23.88
31.04
(76.92%)
📈 view plot
🚷 view threshold
215.22
(0.00%)Baseline: 215.22
279.79
(76.92%)
📈 view plot
🚷 view threshold
318.90
(0.00%)Baseline: 318.90
223.23
(70.00%)
/server_side_hello_world: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
350.58
(0.00%)Baseline: 350.58
455.75
(76.92%)
📈 view plot
🚷 view threshold
432.11
(0.00%)Baseline: 432.11
561.74
(76.92%)
📈 view plot
🚷 view threshold
4,084.55
(0.00%)Baseline: 4,084.55
5,309.91
(76.92%)
📈 view plot
🚷 view threshold
23.57
(0.00%)Baseline: 23.57
16.50
(70.00%)
/server_side_hello_world: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
18.64
(0.00%)Baseline: 18.64
24.23
(76.92%)
📈 view plot
🚷 view threshold
32.74
(0.00%)Baseline: 32.74
42.56
(76.92%)
📈 view plot
🚷 view threshold
205.16
(0.00%)Baseline: 205.16
266.71
(76.92%)
📈 view plot
🚷 view threshold
315.73
(0.00%)Baseline: 315.73
221.01
(70.00%)
/server_side_hello_world_es5: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
289.17
(0.00%)Baseline: 289.17
375.92
(76.92%)
📈 view plot
🚷 view threshold
388.45
(0.00%)Baseline: 388.45
504.99
(76.92%)
📈 view plot
🚷 view threshold
7,324.35
(0.00%)Baseline: 7,324.35
9,521.66
(76.92%)
📈 view plot
🚷 view threshold
17.03
(0.00%)Baseline: 17.03
11.92
(70.00%)
/server_side_hello_world_es5: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
24.02
(0.00%)Baseline: 24.02
31.23
(76.92%)
📈 view plot
🚷 view threshold
32.87
(0.00%)Baseline: 32.87
42.73
(76.92%)
📈 view plot
🚷 view threshold
133.41
(0.00%)Baseline: 133.41
173.43
(76.92%)
📈 view plot
🚷 view threshold
369.55
(0.00%)Baseline: 369.55
258.69
(70.00%)
/server_side_hello_world_hooks: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
281.60
(0.00%)Baseline: 281.60
366.08
(76.92%)
📈 view plot
🚷 view threshold
424.16
(0.00%)Baseline: 424.16
551.41
(76.92%)
📈 view plot
🚷 view threshold
4,238.40
(0.00%)Baseline: 4,238.40
5,509.92
(76.92%)
📈 view plot
🚷 view threshold
23.57
(0.00%)Baseline: 23.57
16.50
(70.00%)
/server_side_hello_world_hooks: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
14.81
(0.00%)Baseline: 14.81
19.25
(76.92%)
📈 view plot
🚷 view threshold
20.58
(0.00%)Baseline: 20.58
26.75
(76.92%)
📈 view plot
🚷 view threshold
368.85
(0.00%)Baseline: 368.85
479.50
(76.92%)
📈 view plot
🚷 view threshold
328.02
(0.00%)Baseline: 328.02
229.61
(70.00%)
/server_side_hello_world_props: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
352.54
(0.00%)Baseline: 352.54
458.30
(76.92%)
📈 view plot
🚷 view threshold
435.15
(0.00%)Baseline: 435.15
565.69
(76.92%)
📈 view plot
🚷 view threshold
3,800.34
(0.00%)Baseline: 3,800.34
4,940.44
(76.92%)
📈 view plot
🚷 view threshold
23.62
(0.00%)Baseline: 23.62
16.53
(70.00%)
/server_side_hello_world_shared_store: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
706.88
(0.00%)Baseline: 706.88
918.94
(76.92%)
📈 view plot
🚷 view threshold
973.81
(0.00%)Baseline: 973.81
1,265.95
(76.92%)
📈 view plot
🚷 view threshold
8,769.88
(0.00%)Baseline: 8,769.88
11,400.84
(76.92%)
📈 view plot
🚷 view threshold
11.23
(0.00%)Baseline: 11.23
7.86
(70.00%)
/server_side_hello_world_shared_store: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
20.97
(0.00%)Baseline: 20.97
27.26
(76.92%)
📈 view plot
🚷 view threshold
30.88
(0.00%)Baseline: 30.88
40.14
(76.92%)
📈 view plot
🚷 view threshold
212.45
(0.00%)Baseline: 212.45
276.19
(76.92%)
📈 view plot
🚷 view threshold
377.21
(0.00%)Baseline: 377.21
264.05
(70.00%)
/server_side_hello_world_shared_store_controller: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
712.27
(0.00%)Baseline: 712.27
925.95
(76.92%)
📈 view plot
🚷 view threshold
963.20
(0.00%)Baseline: 963.20
1,252.16
(76.92%)
📈 view plot
🚷 view threshold
7,742.64
(0.00%)Baseline: 7,742.64
10,065.43
(76.92%)
📈 view plot
🚷 view threshold
8.64
(0.00%)Baseline: 8.64
6.05
(70.00%)
/server_side_hello_world_shared_store_controller: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
21.05
(0.00%)Baseline: 21.05
27.36
(76.92%)
📈 view plot
🚷 view threshold
30.68
(0.00%)Baseline: 30.68
39.88
(76.92%)
📈 view plot
🚷 view threshold
221.10
(0.00%)Baseline: 221.10
287.43
(76.92%)
📈 view plot
🚷 view threshold
377.31
(0.00%)Baseline: 377.31
264.12
(70.00%)
/server_side_hello_world_shared_store_defer: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
681.85
(0.00%)Baseline: 681.85
886.40
(76.92%)
📈 view plot
🚷 view threshold
929.29
(0.00%)Baseline: 929.29
1,208.08
(76.92%)
📈 view plot
🚷 view threshold
8,630.78
(0.00%)Baseline: 8,630.78
11,220.01
(76.92%)
📈 view plot
🚷 view threshold
11.42
(0.00%)Baseline: 11.42
7.99
(70.00%)
/server_side_hello_world_shared_store_defer: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
21.35
(0.00%)Baseline: 21.35
27.76
(76.92%)
📈 view plot
🚷 view threshold
33.55
(0.00%)Baseline: 33.55
43.61
(76.92%)
📈 view plot
🚷 view threshold
244.83
(0.00%)Baseline: 244.83
318.28
(76.92%)
📈 view plot
🚷 view threshold
369.76
(0.00%)Baseline: 369.76
258.83
(70.00%)
/server_side_hello_world_with_options: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
346.19
(0.00%)Baseline: 346.19
450.05
(76.92%)
📈 view plot
🚷 view threshold
435.92
(0.00%)Baseline: 435.92
566.70
(76.92%)
📈 view plot
🚷 view threshold
3,979.94
(0.00%)Baseline: 3,979.94
5,173.92
(76.92%)
📈 view plot
🚷 view threshold
23.58
(0.00%)Baseline: 23.58
16.51
(70.00%)
/server_side_hello_world_with_options: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
21.00
(0.00%)Baseline: 21.00
27.30
(76.92%)
📈 view plot
🚷 view threshold
32.08
(0.00%)Baseline: 32.08
41.70
(76.92%)
📈 view plot
🚷 view threshold
230.22
(0.00%)Baseline: 230.22
299.29
(76.92%)
📈 view plot
🚷 view threshold
364.21
(0.00%)Baseline: 364.21
254.95
(70.00%)
/server_side_log_throw: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
354.74
(0.00%)Baseline: 354.74
461.16
(76.92%)
📈 view plot
🚷 view threshold
450.51
(0.00%)Baseline: 450.51
585.66
(76.92%)
📈 view plot
🚷 view threshold
4,214.80
(0.00%)Baseline: 4,214.80
5,479.24
(76.92%)
📈 view plot
🚷 view threshold
22.83
(0.00%)Baseline: 22.83
15.98
(70.00%)
/server_side_log_throw: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
18.03
(0.00%)Baseline: 18.03
23.44
(76.92%)
📈 view plot
🚷 view threshold
28.45
(0.00%)Baseline: 28.45
36.98
(76.92%)
📈 view plot
🚷 view threshold
204.64
(0.00%)Baseline: 204.64
266.03
(76.92%)
📈 view plot
🚷 view threshold
434.73
(0.00%)Baseline: 434.73
304.31
(70.00%)
/server_side_log_throw_plain_js: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
272.90
(0.00%)Baseline: 272.90
354.77
(76.92%)
📈 view plot
🚷 view threshold
433.22
(0.00%)Baseline: 433.22
563.19
(76.92%)
📈 view plot
🚷 view threshold
4,962.16
(0.00%)Baseline: 4,962.16
6,450.81
(76.92%)
📈 view plot
🚷 view threshold
22.80
(0.00%)Baseline: 22.80
15.96
(70.00%)
/server_side_log_throw_plain_js: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
16.96
(0.00%)Baseline: 16.96
22.05
(76.92%)
📈 view plot
🚷 view threshold
25.90
(0.00%)Baseline: 25.90
33.67
(76.92%)
📈 view plot
🚷 view threshold
235.20
(0.00%)Baseline: 235.20
305.76
(76.92%)
📈 view plot
🚷 view threshold
455.44
(0.00%)Baseline: 455.44
318.81
(70.00%)
/server_side_log_throw_raise: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
352.59
(0.00%)Baseline: 352.59
458.37
(76.92%)
📈 view plot
🚷 view threshold
444.82
(0.00%)Baseline: 444.82
578.27
(76.92%)
📈 view plot
🚷 view threshold
3,890.05
(0.00%)Baseline: 3,890.05
5,057.07
(76.92%)
📈 view plot
🚷 view threshold
23.06
(0.00%)Baseline: 23.06
16.14
(70.00%)
/server_side_log_throw_raise: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
9.67
(0.00%)Baseline: 9.67
12.57
(76.92%)
📈 view plot
🚷 view threshold
14.34
(0.00%)Baseline: 14.34
18.64
(76.92%)
📈 view plot
🚷 view threshold
102.17
(0.00%)Baseline: 102.17
132.82
(76.92%)
📈 view plot
🚷 view threshold
805.79
(0.00%)Baseline: 805.79
564.05
(70.00%)
/server_side_log_throw_raise_invoker: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
8.02
(0.00%)Baseline: 8.02
10.43
(76.92%)
📈 view plot
🚷 view threshold
17.17
(0.00%)Baseline: 17.17
22.32
(76.92%)
📈 view plot
🚷 view threshold
69.74
(0.00%)Baseline: 69.74
90.66
(76.92%)
📈 view plot
🚷 view threshold
858.25
(0.00%)Baseline: 858.25
600.78
(70.00%)
/server_side_log_throw_raise_invoker: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
16.30
(0.00%)Baseline: 16.30
21.19
(76.92%)
📈 view plot
🚷 view threshold
25.69
(0.00%)Baseline: 25.69
33.40
(76.92%)
📈 view plot
🚷 view threshold
176.95
(0.00%)Baseline: 176.95
230.03
(76.92%)
📈 view plot
🚷 view threshold
483.40
(0.00%)Baseline: 483.40
338.38
(70.00%)
/server_side_redux_app: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
367.17
(0.00%)Baseline: 367.17
477.32
(76.92%)
📈 view plot
🚷 view threshold
455.38
(0.00%)Baseline: 455.38
591.99
(76.92%)
📈 view plot
🚷 view threshold
4,149.93
(0.00%)Baseline: 4,149.93
5,394.91
(76.92%)
📈 view plot
🚷 view threshold
22.59
(0.00%)Baseline: 22.59
15.81
(70.00%)
/server_side_redux_app: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
21.57
(0.00%)Baseline: 21.57
28.04
(76.92%)
📈 view plot
🚷 view threshold
33.58
(0.00%)Baseline: 33.58
43.65
(76.92%)
📈 view plot
🚷 view threshold
242.52
(0.00%)Baseline: 242.52
315.28
(76.92%)
📈 view plot
🚷 view threshold
366.53
(0.00%)Baseline: 366.53
256.57
(70.00%)
/server_side_redux_app_cached: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
9.96
(0.00%)Baseline: 9.96
12.95
(76.92%)
📈 view plot
🚷 view threshold
18.08
(0.00%)Baseline: 18.08
23.50
(76.92%)
📈 view plot
🚷 view threshold
82.60
(0.00%)Baseline: 82.60
107.38
(76.92%)
📈 view plot
🚷 view threshold
748.44
(0.00%)Baseline: 748.44
523.91
(70.00%)
/server_side_redux_app_cached: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
19.41
(0.00%)Baseline: 19.41
25.23
(76.92%)
📈 view plot
🚷 view threshold
26.74
(0.00%)Baseline: 26.74
34.76
(76.92%)
📈 view plot
🚷 view threshold
224.72
(0.00%)Baseline: 224.72
292.14
(76.92%)
📈 view plot
🚷 view threshold
305.80
(0.00%)Baseline: 305.80
214.06
(70.00%)
/stream_async_components: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
18.01
(0.00%)Baseline: 18.01
23.41
(76.92%)
📈 view plot
🚷 view threshold
35.09
(0.00%)Baseline: 35.09
45.62
(76.92%)
📈 view plot
🚷 view threshold
195.78
(0.00%)Baseline: 195.78
254.51
(76.92%)
📈 view plot
🚷 view threshold
326.65
(0.00%)Baseline: 326.65
228.65
(70.00%)
/turbo_frame_tag_hello_world: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
8.22
(0.00%)Baseline: 8.22
10.69
(76.92%)
📈 view plot
🚷 view threshold
19.11
(0.00%)Baseline: 19.11
24.84
(76.92%)
📈 view plot
🚷 view threshold
76.85
(0.00%)Baseline: 76.85
99.91
(76.92%)
📈 view plot
🚷 view threshold
812.33
(0.00%)Baseline: 812.33
568.63
(70.00%)
/turbolinks_cache_disabled: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
10.17
(0.00%)Baseline: 10.17
13.22
(76.92%)
📈 view plot
🚷 view threshold
21.29
(0.00%)Baseline: 21.29
27.68
(76.92%)
📈 view plot
🚷 view threshold
127.39
(0.00%)Baseline: 127.39
165.61
(76.92%)
📈 view plot
🚷 view threshold
650.39
(0.00%)Baseline: 650.39
455.27
(70.00%)
/turbolinks_cache_disabled: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
16.17
(0.00%)Baseline: 16.17
21.02
(76.92%)
📈 view plot
🚷 view threshold
25.94
(0.00%)Baseline: 25.94
33.72
(76.92%)
📈 view plot
🚷 view threshold
185.42
(0.00%)Baseline: 185.42
241.05
(76.92%)
📈 view plot
🚷 view threshold
362.28
(0.00%)Baseline: 362.28
253.60
(70.00%)
/xhr_refresh: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
687.60
(0.00%)Baseline: 687.60
893.88
(76.92%)
📈 view plot
🚷 view threshold
931.75
(0.00%)Baseline: 931.75
1,211.28
(76.92%)
📈 view plot
🚷 view threshold
8,097.52
(0.00%)Baseline: 8,097.52
10,526.78
(76.92%)
📈 view plot
🚷 view threshold
11.58
(0.00%)Baseline: 11.58
8.11
(70.00%)
/xhr_refresh: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
21.37
(0.00%)Baseline: 21.37
27.78
(76.92%)
📈 view plot
🚷 view threshold
33.15
(0.00%)Baseline: 33.15
43.09
(76.92%)
📈 view plot
🚷 view threshold
169.55
(0.00%)Baseline: 169.55
220.41
(76.92%)
📈 view plot
🚷 view threshold
372.12
(0.00%)Baseline: 372.12
260.48
(70.00%)
Pro Node Renderer: react_ssr (non-RSC)📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
4.76
(0.00%)Baseline: 4.76
6.19
(76.92%)
📈 view plot
🚷 view threshold
10.14
(0.00%)Baseline: 10.14
13.18
(76.92%)
📈 view plot
🚷 view threshold
16.47
(0.00%)Baseline: 16.47
21.41
(76.92%)
📈 view plot
🚷 view threshold
1,649.86
(0.00%)Baseline: 1,649.86
1,154.90
(70.00%)
Pro Node Renderer: simple_eval (non-RSC)📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
4.53
(0.00%)Baseline: 4.53
5.89
(76.92%)
📈 view plot
🚷 view threshold
13.04
(0.00%)Baseline: 13.04
16.95
(76.92%)
📈 view plot
🚷 view threshold
20.23
(0.00%)Baseline: 20.23
26.30
(76.92%)
📈 view plot
🚷 view threshold
1,431.69
(0.00%)Baseline: 1,431.69
1,002.18
(70.00%)
🐰 View full continuous benchmarking report in Bencher

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

PR Review: Remove license requirement for React on Rails Pro

Summary

This PR successfully transitions React on Rails Pro from a "license-required" to "license-optional with attribution" model. The implementation is clean, well-tested, and maintains backward compatibility while achieving the stated goal.

✅ Strengths

1. Excellent API Design

  • Clear status-based API (license_status returning :valid, :expired, :invalid, :missing)
  • Consistent behavior across Ruby and TypeScript implementations
  • Non-blocking validation that never crashes the app

2. Comprehensive Test Coverage

  • Both Ruby and TypeScript have thorough test suites covering all license states
  • Tests verify caching behavior, logging, and edge cases
  • Good use of mocks to isolate license validation logic

3. Security Best Practices

  • ✅ JWT signature verification remains enabled (verify: true, algorithms: ['RS256'])
  • ✅ No bypassing of security checks
  • ✅ Manual expiration handling maintains proper validation flow
  • ✅ Prevents "alg=none" attacks through explicit algorithm specification

4. Clean Code

  • Well-documented methods with clear return types
  • Consistent logging across Ruby and TypeScript
  • Proper separation of concerns (loading, decoding, validation)

🔍 Code Quality Observations

Ruby Implementation (license_validator.rb)

Strengths:

  • Clear documentation with YARD tags
  • Proper caching with defined? checks
  • Good error handling that never raises

Minor notes:

  • Line 93: Comment "NEVER set to false!" is good security reminder
  • Line 95: Manual expiration handling is correctly implemented

TypeScript Implementation (licenseValidator.ts)

Strengths:

  • Proper TypeScript interfaces for type safety
  • Module-level caching mirrors Ruby implementation
  • Consistent error messages with Ruby version

Minor observations:

  • Lines 93-94: Good use of ignoreExpiration: true with manual checking
  • Line 90: algorithms: ['RS256'] correctly prevents downgrade attacks

Engine Integration (engine.rb)

Excellent approach:

  • Non-blocking initialization (lines 13-28)
  • Clear status-based logging
  • Consistent messaging with TypeScript version

🛡️ Security Review

✅ Secure JWT Validation

Both Ruby and TypeScript implementations correctly:

  • Enforce RS256 algorithm only
  • Verify signatures before trusting data
  • Handle expiration separately (non-security concern in this context)

The manual expiration handling gives control over grace periods while maintaining security.

📋 Test Coverage Analysis

Ruby Tests (license_validator_spec.rb)

Excellent coverage of:

  • ✅ Valid license (lines 49-64)
  • ✅ Expired license (lines 66-80)
  • ✅ Missing expiration field (lines 82-103)
  • ✅ Invalid signature (lines 105-120)
  • ✅ Missing license (lines 122-135)
  • ✅ License from file (lines 137-152)
  • ✅ Caching behavior (lines 59-63, 226-237)

Test quality: Very thorough, uses proper mocking, tests both happy and error paths.

TypeScript Tests (licenseValidator.test.ts)

Mirrors Ruby test coverage:

  • ✅ All license states tested
  • ✅ Caching verified (lines 158-176)
  • ✅ Proper use of jest.resetModules() for isolation
  • ✅ Mock setup is clean and consistent

🎯 Integration Points

Open Source Integration (utils.rb)

Lines 254-263: Good separation - the open-source side only checks gem presence, not license validity. This is correct for the new model.

Pro Attribution Comment (utils.rb:208-225)

Excellent implementation:

  • Clean switch statement based on license status
  • Consistent HTML comment format
  • Easy to verify in page source

🐛 Potential Issues

None Critical - All Minor Observations:

  1. Logging Consistency (Very minor)

    • Ruby: Rails.logger.warn vs TypeScript: console.warn
    • This is appropriate for each platform, just noting the difference
  2. Expiration Days Calculation

    • Ruby: ((current_time - exp_time) / (24 * 60 * 60)).to_i
    • TypeScript: Math.floor((currentTime - expTime) / (24 * 60 * 60))
    • Both correct, TypeScript could use a constant for readability (optional)

✨ Recommendations

For This PR (Optional - Not Blocking):

  1. Consider adding a constant for seconds per day in both implementations for readability

  2. TypeScript: Consider distinguishing file errors - current approach (treat all errors as "no license") is acceptable, but could be more specific

For Future (Post-Merge):

  1. Monitoring: Consider adding metrics for license status distribution
  2. Analytics: The logged plan and iss fields are good for analytics
  3. Documentation: Update user-facing docs about the new licensing model

🎉 Verdict

APPROVED

This is a well-implemented, secure, and thoroughly tested change. The transition from required to optional licensing is handled gracefully with:

  • No security regressions
  • Excellent test coverage
  • Clean, maintainable code
  • Clear user messaging
  • Proper separation of concerns

The PR successfully achieves its goal while maintaining code quality and security standards.

📊 Final Metrics

  • Lines Added: 447
  • Lines Removed: 563
  • Net Change: -116 (cleaner codebase!)
  • Files Changed: 10
  • Test Coverage: Comprehensive for all license states
  • Security: All JWT validation best practices followed
  • Breaking Changes: None (backward compatible)

Great work! This PR is ready to merge. 🚀

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

PR Review: Remove license requirement for React on Rails Pro

Overview

This is a significant and well-executed refactoring that transforms React on Rails Pro from a "license-required" model to a "license-optional with attribution" model. The changes are comprehensive, touching both Ruby and TypeScript codebases, with thorough test coverage.

Strengths

1. Excellent Architecture & Design

  • Consistent API across Ruby and TypeScript: Both implementations follow the same pattern with license_status/getLicenseStatus(), licensed?/isLicensed(), and license_data/getLicenseData()
  • Clear status model: The four states (:valid, :expired, :invalid, :missing) are well-defined and consistent
  • Non-throwing design: The "never raises" contract is consistently maintained throughout both implementations
  • Proper separation of concerns: Loading, decoding, and expiration checking are cleanly separated

2. Security Best Practices

The security aspects are handled correctly with proper signature verification and RS256 enforcement. Comments explain security rationale clearly.

3. Comprehensive Testing

  • Ruby tests cover all status transitions and caching behavior
  • TypeScript tests mirror the Ruby test structure (14 tests)
  • Tests verify caching works correctly
  • Edge cases (missing exp field, invalid signature) are well covered

4. Documentation & User Experience

  • HTML attribution comments clearly show license status
  • Log messages are helpful and include actionable URLs
  • Comments explain design decisions

Issues & Concerns

1. Performance: Redundant Status Checks in license_data

Location: Both license_validator.rb:31-36 and licenseValidator.ts:184-188

Problem: The license_data method can trigger redundant work. If someone calls license_status first (returning :missing), then calls license_data, the full determination runs again because @license_data was never cached.

Fix: Cache @license_data = nil explicitly for missing/invalid cases in determine_license_status:

unless license_string
  log_license_warning("No license found. Running in unlicensed mode.")
  @license_data = nil  # Cache nil explicitly
  return :missing
end

decoded_data = decode_license(license_string)
unless decoded_data
  @license_data = nil  # Cache nil explicitly
  return :invalid
end

Same for TypeScript implementation.

2. Minor: Missing Test Coverage for File Read Errors

The exception handling added in commit 8/8 for File.read isn't tested. Suggest adding:

context "when license file exists but cannot be read" do
  before do
    ENV.delete("REACT_ON_RAILS_PRO_LICENSE")
    allow(config_file_path).to receive(:exist?).and_return(true)
    allow(File).to receive(:read).and_raise(Errno::EACCES, "Permission denied")
  end

  it "returns :missing" do
    expect(described_class.license_status).to eq(:missing)
  end
end

3. Code Quality: Unused Constant

VALID_STATUSES constant (line 12) is defined but never used. Either remove it, use it for validation, or add a comment explaining it's for documentation.

4. Security: HTML Comment Information Disclosure

License status is now exposed in HTML:

<!-- Powered by React on Rails Pro (c) ShakaCode | UNLICENSED -->

This reveals to anyone viewing source whether the site is unlicensed/expired/invalid. This is likely intentional (compliance mechanism), but should be documented in CHANGELOG_PRO.md as a behavior change.

Recommendations

Before Merging (Required):

  1. Fix license_data performance issue - Cache nil explicitly for missing/invalid states

Before Merging (Recommended):

  1. ⚠️ Add test for File.read exceptions
  2. 📋 Decide on VALID_STATUSES constant - Remove, use, or document
  3. 📝 Document HTML comment disclosure in CHANGELOG_PRO.md
  4. Run linters: bundle exec rubocop and pnpm run lint
  5. Complete manual test plan - Verify HTML comments in browser

Overall Assessment

Quality: ⭐⭐⭐⭐⭐ (5/5)

This is an excellent PR demonstrating:

  • ✅ Strong architectural design
  • ✅ Thorough testing
  • ✅ Good security practices
  • ✅ Clear documentation
  • ✅ Consistent implementation across Ruby and TypeScript
  • ✅ Well-structured commits (8 logical commits)

Recommendation: Approve with minor changes requested

The caching performance issue (#1) should be fixed before merge. Other suggestions improve quality but aren't blockers. Once addressed, this is ready to merge. Great work! 🎉

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Code Review - PR #2324: Remove License Requirement for React on Rails Pro

Thank you for this comprehensive refactor! This is a significant architectural change that fundamentally alters the licensing model. Overall, the implementation is well-structured and follows best practices. Here's my detailed feedback:


Strengths

1. Excellent Separation of Concerns

  • Clean separation between license validation logic and application startup
  • Both Ruby and TypeScript implementations follow identical patterns
  • Clear public API: license_status, licensed?, and license_data methods

2. Robust Error Handling

  • All error cases are handled gracefully without throwing exceptions
  • Appropriate use of try/catch in TypeScript and rescue in Ruby
  • Non-blocking behavior ensures application always starts

3. Strong Test Coverage

  • Comprehensive test cases covering all status types: valid, expired, invalid, missing
  • Tests verify caching behavior and reset functionality
  • Both Ruby (RSpec) and TypeScript (Jest) test suites are well-structured

4. Security Best Practices

  • ✅ Correct JWT verification: verify: true in Ruby, algorithms: ['RS256'] in TypeScript
  • ✅ Manual expiration handling: Prevents bypassing security checks
  • ✅ No algorithm downgrade attacks: Only RS256 is allowed

5. Good Caching Strategy

  • License status is cached to avoid repeated file I/O and JWT verification
  • reset\! method provided for testing purposes
  • Cache invalidation is handled properly

⚠️ Issues & Recommendations

1. Critical: Race Condition in Caching (Ruby)

Location: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb:10-14

Issue: The caching pattern has a potential race condition:

def license_status
  return @license_status if defined?(@license_status)
  @license_status = determine_license_status
end

In multi-threaded environments (Puma, Sidekiq), multiple threads could enter determine_license_status simultaneously before @license_status is set.

Recommendation: Use Ruby's built-in thread-safe memoization pattern:

def license_status
  @license_status ||= determine_license_status
end

Or for more explicit thread safety:

@mutex = Mutex.new

def license_status
  @mutex.synchronize do
    @license_status ||= determine_license_status
  end
end

Same issue exists in:

  • license_data method (line 24-30)
  • TypeScript getLicenseStatus function (line 165-172)

2. Security: Missing Input Validation

Location: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb:93-105

Issue: The license string is read from environment/file without size validation. A maliciously large file could cause memory issues.

Recommendation: Add size validation:

def load_license_string
  license = ENV.fetch("REACT_ON_RAILS_PRO_LICENSE", nil)
  return validate_license_size(license) if license.present?

  config_path = Rails.root.join("config", "react_on_rails_pro_license.key")
  if config_path.exist?
    begin
      license = File.read(config_path, 10_000) # Limit to 10KB
      return validate_license_size(license.strip)
    rescue StandardError => e
      log_license_warning("Failed to read license file: #{e.message}")
    end
  end

  nil
end

def validate_license_size(license)
  if license.bytesize > 10_000
    log_license_warning("License size exceeds maximum (10KB)")
    return nil
  end
  license
end

Same issue in TypeScript: licenseValidator.ts:62-80


3. Type Safety: Optional vs. Required Fields

Location: packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts:6-19

Issue: The interface makes exp optional (exp?: number), but it's actually required for valid licenses.

Recommendation: Split into two interfaces for better type safety:

interface BaseLicenseData {
  sub?: string;
  iat?: number;
  plan?: string;
  iss?: string;
  [key: string]: unknown;
}

interface ValidLicenseData extends BaseLicenseData {
  exp: number; // Required for valid licenses
}

type LicenseData = ValidLicenseData | BaseLicenseData;

This makes the type system enforce what the code actually expects.


4. Code Duplication: DRY Violation

Location: Multiple files

Issue: License loading logic is duplicated between Ruby and TypeScript with subtle differences.

Recommendation: Document the parity requirements explicitly:

  • Add a comment at the top of both files: // IMPORTANT: Keep in sync with Ruby/TypeScript implementation
  • Consider adding a test that verifies both implementations produce identical results for the same inputs

5. Missing Edge Case: Empty License String

Location: Both Ruby and TypeScript implementations

Issue: What happens if the license file exists but is empty, or contains only whitespace?

Current behavior: strip in Ruby and trim in TypeScript will return empty string, which then gets passed to JWT decoder.

Recommendation: Add explicit check:

license = File.read(config_path).strip
return nil if license.blank? # Add this check
const licenseString = fs.readFileSync(configPath, 'utf8').trim();
if (\!licenseString) return undefined; // Add this check

6. Logging Inconsistency

Location:

  • engine.rb:19 uses Rails.logger.info
  • license_validator.rb:133 uses Rails.logger.warn
  • master.ts:12 uses log.info

Issue: Different logging mechanisms are used in different contexts.

Recommendation: Standardize on a single logging approach, or document why different loggers are used. Consider:

# Use structured logging for better observability
Rails.logger.info(
  "[React on Rails Pro] License validation",
  status: :valid,
  plan: license_data["plan"]
)

7. Performance: Redundant File System Checks

Location: licenseValidator.ts:70-74 and license_validator.rb:78-85

Issue: The code checks if file exists, then reads it. If the file is deleted between the check and read, an exception is thrown.

Recommendation: Use try/catch pattern directly without existence check:

def load_license_string
  license = ENV.fetch("REACT_ON_RAILS_PRO_LICENSE", nil)
  return license if license.present?

  config_path = Rails.root.join("config", "react_on_rails_pro_license.key")
  begin
    File.read(config_path).strip
  rescue Errno::ENOENT
    nil # File doesn't exist, return nil
  rescue StandardError => e
    log_license_warning("Failed to read license file: #{e.message}")
    nil
  end
end

This is more idiomatic Ruby/Node.js and handles race conditions.


8. Documentation: Missing Upgrade Path

Issue: Existing users will need to understand how this change affects them.

Recommendation: Add migration documentation:

  • What happens to existing grace period configurations?
  • Do users need to change any code?
  • Are there any breaking changes in the Pro gem API?

Consider adding to CHANGELOG_PRO.md:

### Breaking Changes
- Removed `GRACE_PERIOD_SECONDS` constant - no longer used
- License validation no longer throws exceptions or exits the process
- Free evaluation license concept has been removed

### Migration Guide
- **No action required for most users** - the app will continue working
- If you have custom error handling around license validation, update it to check `LicenseValidator.license_status`
- If you relied on the app crashing on invalid license, implement custom checks using `LicenseValidator.licensed?`

🔍 Testing Recommendations

1. Add Integration Tests

Current tests are unit tests. Consider adding integration tests that verify:

  • HTML comment appears correctly in rendered output
  • License status is checked on Rails startup
  • Node renderer logs appropriate messages on startup

2. Add Thread Safety Tests

Verify that concurrent calls to license_status don't cause issues:

it "handles concurrent access safely" do
  threads = 10.times.map do
    Thread.new { described_class.license_status }
  end
  statuses = threads.map(&:value)
  expect(statuses.uniq.size).to eq(1) # All threads see same status
end

3. Add File Permission Tests

Test behavior when license file exists but isn't readable (permissions issue).


📊 Performance Considerations

  1. Caching is good: License validation happens once per process lifetime
  2. No performance regression: Removing grace period calculations actually simplifies logic
  3. JWT verification cost: RS256 verification is ~1-2ms, acceptable for startup-time check

🔒 Security Assessment

No security regressions identified

  • JWT verification remains secure
  • No new attack vectors introduced
  • Graceful degradation doesn't compromise security
  • HTML comments don't leak sensitive data

📝 Code Style & Best Practices

Ruby Style

  • ✅ Follows RuboCop conventions
  • ✅ Good use of Ruby idioms (fetch, present?, symbols)
  • ✅ Proper error handling with rescue blocks

TypeScript Style

  • ✅ Proper type annotations
  • ✅ Clear function documentation
  • ✅ Good separation of concerns

Minor Nitpicks

  1. Line 213-222 in utils.rb: Consider extracting status-to-message mapping into a constant:
LICENSE_STATUS_MESSAGES = {
  valid: "Licensed",
  expired: "LICENSE EXPIRED",
  invalid: "INVALID LICENSE",
  missing: "UNLICENSED"
}.freeze

def self.pro_attribution_comment
  base = "Powered by React on Rails Pro (c) ShakaCode"
  status_msg = LICENSE_STATUS_MESSAGES[LicenseValidator.license_status]
  "<\!-- #{base} | #{status_msg} -->"
end

Final Verdict

This is a well-executed refactor that achieves its goals. The critical issues identified are:

Must Fix (before merge):

  1. ⚠️ Race condition in caching (Ruby license_status method)
  2. ⚠️ Missing input size validation for license strings

Should Fix (can be follow-up):

  1. Type safety improvements in TypeScript interfaces
  2. Empty string edge case handling
  3. Migration documentation in CHANGELOG

Nice to Have:

  1. Logging standardization
  2. File system check optimization
  3. Thread safety tests
  4. Extract constants for status messages

🎯 Action Items

  1. Address race condition in caching (Priority: High)
  2. Add input size validation (Priority: High)
  3. Update CHANGELOG_PRO.md with migration notes (Priority: Medium)
  4. Add integration tests for HTML attribution (Priority: Medium)
  5. Consider splitting TypeScript interfaces for better type safety (Priority: Low)

Overall Assessment: Approve with changes

The core logic is sound, tests are comprehensive, and the security model is preserved. Once the caching race condition and input validation are addressed, this is ready to merge.

Great work on a complex refactor! 🚀

Copy link
Copy Markdown
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

Caution

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

⚠️ Outside diff range comments (1)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)

32-46: Restore original ENV value to avoid test pollution.

The suite deletes REACT_ON_RAILS_PRO_LICENSE without preserving the original value. If this variable was set before the test, the deletion will persist after the test completes, affecting subsequent tests or the host environment.

🧪 Suggested fix
 before do
   described_class.reset!
   stub_const("ReactOnRailsPro::LicensePublicKey::KEY", test_public_key)
+  `@original_license` = ENV["REACT_ON_RAILS_PRO_LICENSE"]
   ENV.delete("REACT_ON_RAILS_PRO_LICENSE")
   allow(Rails).to receive_messages(logger: mock_logger, root: mock_root)
 end

 after do
   described_class.reset!
-  ENV.delete("REACT_ON_RAILS_PRO_LICENSE")
+  if `@original_license`
+    ENV["REACT_ON_RAILS_PRO_LICENSE"] = `@original_license`
+  else
+    ENV.delete("REACT_ON_RAILS_PRO_LICENSE")
+  end
 end

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Code Review: License Optional with Attribution

This is a significant architectural change that shifts React on Rails Pro from a license-enforced to an attribution-based licensing model. Overall, the implementation is well-structured and thorough. Here are my findings:


✅ Strengths

1. Excellent Code Quality

  • Clean separation of concerns between Ruby and TypeScript implementations
  • Both implementations mirror each other's logic, making maintenance easier
  • Comprehensive test coverage for all license states (:valid, :expired, :invalid, :missing)

2. Security Best Practices

  • JWT verification is strict and secure:
    • Ruby: verify: true with algorithm: 'RS256' (line 64-69 in license_validator.rb)
    • TypeScript: algorithms: ['RS256'] prevents algorithm downgrade attacks (line 76 in licenseValidator.ts)
    • Both disable automatic expiration checking to handle it manually
  • Comment explicitly warns against changing verification settings (line 67 in license_validator.rb)

3. Non-Blocking Architecture

  • Application always starts, regardless of license status ✅
  • Graceful degradation with informative warnings
  • Caching prevents repeated validation overhead

4. Consistent Attribution

  • HTML comments clearly indicate license status across all scenarios
  • Attribution format is professional and unobtrusive

5. Test Coverage

  • Both Ruby and TypeScript have comprehensive test suites
  • Tests cover all edge cases: valid, expired, invalid signature, missing license, file read errors
  • Caching behavior is properly tested
  • Reset functionality verified for testing isolation

🔍 Observations & Minor Suggestions

1. Typo in Comment (knip.ts:76)

// License validator: reset() is used in tests, getLicenseStatus/LicenseStatus imported by master.ts

✅ This accurately describes the exports and is helpful for understanding the code's usage.

2. Security Consideration: File Path Traversal

In both loadLicenseString implementations, the config file path is constructed with:

  • Ruby: Rails.root.join("config", "react_on_rails_pro_license.key")
  • TypeScript: path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key')

These are safe because they use hardcoded path components. However, if this were ever changed to accept user input, consider validating the path doesn't contain ../ or absolute paths.

Current code is secure ✅ - just noting for future reference.

3. Consistent Error Messages

The warning messages are well-structured and consistent across Ruby and TypeScript:

  • "No license found. Running in unlicensed mode."
  • "License expired X day(s) ago. Running in unlicensed mode."
  • "Invalid license signature: [error]. Running in unlicensed mode."

Suggestion: Consider adding a link to the license purchase page in the "No license found" warning (similar to what the Engine does). This would make it easier for users to get licensed.

Example:

"No license found. Running in unlicensed mode. Get a license at https://www.shakacode.com/react-on-rails-pro/"

4. Cache Invalidation

Both implementations cache the license status. The reset! / reset() methods are provided for testing, but there's no way to invalidate the cache at runtime.

Question: What happens if a user updates their license while the app is running?

  • Ruby: App restart required (standard for Rails)
  • Node Renderer: Master process restart required

This is likely acceptable for your use case, but worth documenting if not already covered elsewhere.

5. Open Source Helper Method

In react_on_rails/lib/react_on_rails/utils.rb, the react_on_rails_pro? method (line 259) only checks gem presence, not license validity:

# Note: This only checks gem presence, not license validity

This is correct behavior for the open-source package, but the comment could be clearer:

# Checks if React on Rails Pro gem is installed
# Note: This only checks gem presence, NOT license validity or status.
# To check license status, use ReactOnRailsPro::LicenseValidator.license_status

🎯 Architecture Review

License Validation Flow

  1. First call: Load license → Decode JWT → Check expiration → Cache result
  2. Subsequent calls: Return cached result
  3. Testing: reset! / reset() clears cache

This is efficient and well-designed. ✅

Attribution Generation (Pro Utils)

The pro_attribution_comment method (utils.rb:204-219) generates the HTML comment based on cached license status. This ensures consistency across all rendered pages.

Question: Is this comment injected into every server-rendered component? If so, is there any performance concern with calling LicenseValidator.license_status on every render? (The cache makes this cheap, just confirming it's not called repeatedly per request.)


🚨 Potential Issues

1. No Breaking Issues Found

This PR doesn't introduce any security vulnerabilities, logic errors, or breaking changes. The migration path from "license-required" to "license-optional" is clean.

2. Backward Compatibility

Existing users with valid licenses continue to work as before. Users without licenses now get a functioning app instead of a crash. This is a pure improvement. ✅


📝 Documentation & Changelog

Changelog Entries Needed

As per CLAUDE.md guidelines, this PR should update CHANGELOG_PRO.md (Pro-only changes):

Suggested entry:

#### Breaking Changes

- [PR 2324](https://github.com/shakacode/react_on_rails/pull/2324) by [AbanoubGhadban](https://github.com/AbanoubGhadban): Changed React on Rails Pro licensing model from "license-required" to "license-optional with attribution". Apps now run regardless of license status, displaying an HTML comment indicating license state. Removed grace period logic and free evaluation license concept. **Migration:** No action required - apps with expired or missing licenses will continue running with an attribution comment instead of crashing.

Documentation Updates

Consider documenting:

  • How license validation works (environment variable vs file)
  • What the HTML attribution comments mean
  • How to verify license status in logs
  • Restart requirements for license changes

✅ Testing Recommendations

Manual Testing Checklist (from PR description)

The test plan looks good. I'd add:

  • Verify attribution comment appears in HTML source for each status
  • Check log output for each license state (valid, expired, invalid, missing)
  • Test license file read errors (permissions, corrupted file, invalid JSON)
  • Verify caching behavior (license status doesn't change within same process)
  • Test environment variable precedence (ENV should override file)

CI Testing

All existing tests should pass. The new specs comprehensively cover the changed behavior. ✅


🏆 Overall Assessment

Code Quality: ⭐⭐⭐⭐⭐ (Excellent)
Security: ⭐⭐⭐⭐⭐ (Secure)
Test Coverage: ⭐⭐⭐⭐⭐ (Comprehensive)
Architecture: ⭐⭐⭐⭐⭐ (Well-designed)

Summary

This is a high-quality PR that achieves its goal of making React on Rails Pro license-optional while maintaining attribution. The implementation is secure, well-tested, and follows best practices. No blocking issues found.

Recommendations

  1. Approve and merge once the test plan is verified
  2. Update CHANGELOG_PRO.md with breaking changes entry
  3. Consider the minor documentation/comment improvements suggested above
  4. Verify the HTML attribution comment injection doesn't cause performance issues (likely not an issue due to caching)

Excellent work! 🎉

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Code Review: PR 2324

Great work on this architectural change! Here is my detailed review:

Strengths

  1. Excellent JWT security - signature verification enforced, RS256 algorithm prevents attacks
  2. Well-structured API - four clear states (valid, expired, invalid, missing)
  3. Good test coverage - all states covered, proper test key pairs used
  4. Proper dead code cleanup - removed unused methods and caching
  5. Professional logging - TypeScript now uses Pino logger

Issues and Recommendations

1. Caching patterns differ - TypeScript uses !== undefined, Ruby uses defined?. Consider documenting why caching is safe.

2. Missing validation - No check for empty/malformed license strings. Add pre-check for clearer errors.

3. Inconsistent messages - Ruby mentions Running in unlicensed mode but TypeScript file read errors do not.

4. HTML comment injection risk - Low likelihood but consider escaping if future changes add user data.

5. Timezone handling - Use Time.now.utc.to_i for clarity or document UTC is implicit.

6. Test coverage gap - Add TypeScript test for file read failures matching Ruby tests.

Security Assessment

Overall: Good security posture. JWT verification mandatory, algorithm enforced, no sensitive data exposure.

Approval

LGTM with minor suggestions. Issues are minor, none blocking. Ensure CI passes and consider test additions.

Great work maintaining security in the new licensing model!

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Code Review: License-Optional Attribution Model

I've completed a thorough review of this PR. Overall, this is a well-designed, security-conscious refactoring that successfully transitions from a "crash-on-invalid-license" model to a "warn-and-continue" model. The code quality is excellent with strong test coverage.


✅ Strengths

1. Security Best Practices

  • ✅ JWT verification correctly enforces RS256 algorithm only (prevents alg=none attacks)
  • ✅ Signature verification is never disabled (verify: true in Ruby, enforced in TS)
  • ✅ Proper separation between decoding/verification and expiration checking
  • ✅ Good defensive handling of missing/malformed fields (exp, plan)

2. Clean Architecture

  • ✅ Excellent separation of concerns: load → decode → validate plan → check expiration
  • ✅ Status-based API (:valid, :expired, :invalid, :missing) is clear and extensible
  • ✅ Parallel Ruby/TypeScript implementations are well-aligned
  • ✅ Proper caching with reset! for testing

3. Code Quality

  • ✅ Removed dead code (licensed?, license_data, logLicenseInfo)
  • ✅ Consistent error handling (no exceptions, just status returns)
  • ✅ Good documentation comments explaining behavior
  • ✅ Replaced console.warn with proper Pino logger in TS

4. Test Coverage

  • ✅ Comprehensive test coverage for all license states
  • ✅ Tests for edge cases (missing exp, invalid plans, file read errors)
  • ✅ Proper mocking of logger to verify warning messages
  • ✅ Tests for backwards compatibility (no plan field = valid)

🔍 Areas for Consideration

1. Production Enforcement Gap (Medium Priority)

The new model warns but doesn't prevent production use without a valid license. This shifts enforcement from technical (app crashes) to legal (license violation warnings).

Current behavior:

  • Production deploys work without license
  • Warnings logged but easily missed
  • HTML comment visible but not blocking

Potential issues:

  • Users might accidentally deploy to production without noticing warnings
  • No automated way to enforce license requirement in production

Suggestions:

  • Consider adding REACT_ON_RAILS_PRO_ENFORCE_LICENSE=true env var that exits in production when license is invalid (opt-in strict mode)
  • Or add a rake task for CI: rake react_on_rails_pro:verify_license that exits non-zero if license invalid (developers can run this in CI to prevent prod deploys)
  • Document clearly in LICENSE_SETUP.md that production use requires valid license despite app running

Example:

# In engine.rb initializer
if Rails.env.production? && ENV['REACT_ON_RAILS_PRO_ENFORCE_LICENSE'] == 'true'
  unless status == :valid
    raise ReactOnRailsPro::Error, "Production deployment requires valid license"
  end
end

2. Plan Validation Logic (Low Priority)

The backwards compatibility logic is correct but could be documented more explicitly:

# Current (line 89 in license_validator.rb)
return :valid unless plan # No plan field = valid

# Consider adding explanatory constant:
BACKWARDS_COMPATIBLE_NO_PLAN = true
return :valid unless plan # Old paid licenses have no 'plan' field - treat as valid

Question: Are there legitimate old licenses with plan: "free" that should remain valid? Or is the intent to invalidate all free licenses going forward?

3. Error Handling: File Read Failures (Low Priority)

Line 57-58 in license_validator.rb:

rescue StandardError => e
  log_license_warning("Failed to read license file: #{e.message}. Running in unlicensed mode.")

Current: File permission errors are logged as warnings, license treated as missing.

Consider: Should permission errors be treated as :invalid instead of :missing? A file that exists but can't be read is different from no file at all.

# Possible improvement:
if config_path.exist?
  begin
    return File.read(config_path).strip
  rescue StandardError => e
    log_license_warning("Failed to read license file: #{e.message}. Running in unlicensed mode.")
    return :file_read_error # Distinguish from true :missing
  end
end

4. TypeScript: Process.cwd() Assumption (Low Priority)

Line 56 in licenseValidator.ts:

const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');

Potential issue: If Node process is started from a different directory, process.cwd() might not point to Rails root.

Mitigation: Document that Node renderer must be started from Rails root, or consider using RAILS_ROOT env var if available.


🧪 Testing Recommendations

Before merging, verify these scenarios:

Manual Testing Checklist (from PR description):

  • App starts without license (shows UNLICENSED comment)
  • App starts with expired license (shows LICENSE EXPIRED comment)
  • App starts with valid license (shows Licensed comment)
  • Node renderer starts without license
  • Run full test suite ✅ (appears to be passing based on CodeRabbit summary)

Additional Testing:

  • Verify HTML comments appear in actual rendered output (check view source in browser)
  • Test with license file present but unreadable (chmod 000)
  • Test with license containing plan: "free" to confirm it's rejected
  • Verify Rails logger warnings appear in production logs (not just stdout)

📋 Documentation Updates Needed

Based on the changelog entries, it looks like docs are updated. Verify these are clear:

  1. LICENSE_SETUP.md - Should explicitly state:

    • Production use requires paid license (legal requirement)
    • Development/testing/CI work without license
    • How to verify license status (rails consoleReactOnRailsPro::LicenseValidator.license_status)
  2. CI_SETUP.md - Should state CI no longer needs license

  3. README.md - Should explain new attribution comment behavior


🎯 Summary

This is high-quality work that achieves its goals effectively:

Approve with suggestions - The code is production-ready, secure, and well-tested.

Recommended follow-ups (can be separate PRs):

  1. Add opt-in production enforcement mechanism (ENFORCE_LICENSE env var or rake task)
  2. Add integration test for HTML comment rendering
  3. Consider documenting the plan validation strategy more explicitly

No blocking issues found. The architectural change is sound and implementation is solid.


💡 Minor Nitpicks (Optional)

  1. Line 208 in utils.rb: Consider extracting license status → comment text mapping to constant or method for easier maintenance

  2. Test file organization: The test files are well-organized, but license_status with plan field context (line 188+ in spec) might be clearer as separate describes for each plan value

  3. Changelog: Entry is clear and complete ✅


Recommendation: APPROVE

The security, architecture, and test coverage are excellent. The production enforcement gap is a business/product decision rather than a code quality issue.

Copy link
Copy Markdown
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 `@CHANGELOG.md`:
- Line 28: Remove the changelog entry that begins with "License-Optional
Attribution Model" (the entry referencing PR 2324) from the root CHANGELOG.md
and add it to the Pro changelog instead, keeping only the user-visible
description about React on Rails Pro working without a license for
evaluation/dev/testing/CI and that paid licenses are required for production
(retain the note about old free licenses being treated as
invalid/backwards-compatible plan handling), but remove the clause that mentions
documentation updates (README, LICENSE_SETUP, CI_SETUP); ensure the Pro
changelog entry references PR 2324 and the author attribution as in the
original.

In `@react_on_rails_pro/CHANGELOG.md`:
- Line 24: Update the CHANGELOG entry line to remove the documentation-only
phrase "Documentation overhauled across README, LICENSE_SETUP, and CI_SETUP
guides" so the entry only contains user-visible changes: keep the
License-Optional Attribution Model description, the `plan` field validation
details, the note about old free licenses being invalid, and the PR/author
attribution (the text in the current line that references `"paid"` plan
validation, old free licenses, and PR 2324 by AbanoubGhadban), but delete the
doc-overhaul clause to comply with changelog guidelines.
🧹 Nitpick comments (5)
react_on_rails_pro/CI_SETUP.md (2)

27-71: GitHub Actions workflow example is well-structured.

The example properly separates test and deployment jobs, with clear comments indicating that tests don't need a license while production deployment does. The use of GitHub secrets for the license is appropriate.

Minor suggestion: Consider adding cache: npm or cache: pnpm to the Node setup action for faster CI runs.

⚡ Optional: Add caching to speed up CI
       - name: Set up Node
         uses: actions/setup-node@v3
         with:
           node-version: '18'
+          cache: 'pnpm'

232-242: Troubleshooting guidance should mention the new attribution comment.

According to the PR summary, the application now emits an HTML attribution comment indicating license state (e.g., <!-- Powered by React on Rails Pro (c) ShakaCode | UNLICENSED -->). The troubleshooting section should mention this as a way to verify license status in production.

📝 Suggested addition for verification

Add after line 242:

**Q: How can I verify the license status in production?**
A: Check the HTML source of your rendered pages for the attribution comment:
- `<!-- Powered by React on Rails Pro (c) ShakaCode | LICENSED -->` - Valid paid license
- `<!-- Powered by React on Rails Pro (c) ShakaCode | UNLICENSED -->` - No license present
- `<!-- Powered by React on Rails Pro (c) ShakaCode | LICENSE EXPIRED -->` - License expired
- `<!-- Powered by React on Rails Pro (c) ShakaCode | INVALID LICENSE -->` - Invalid license
react_on_rails_pro/README.md (1)

441-503: FAQ comprehensively addresses licensing questions.

The FAQ section thoroughly covers common licensing questions and technical topics. The answers are clear and actionable.

Consider adding a question about the HTML attribution comment that's generated, as mentioned in the PR summary. This would help users understand how to verify license status at runtime.

react_on_rails_pro/LICENSE_SETUP.md (2)

38-40: CI/CD guidance correctly reflects optional licensing.

The comment "CI works without a license" is accurate and helpful. However, consider rephrasing to be more explicit that licenses are only needed for production deployment steps, not CI test/build steps.

📝 Suggested clarification
-# CI/CD (optional — CI works without a license)
-# Add to your CI environment variables if needed
+# CI/CD tests/builds work without a license
+# Only add license to environment variables for production deployment steps

133-148: JWT format documentation accurately reflects plan field.

The technical details correctly document the optional "plan" field and note that only "paid" is valid for production. This aligns with the plan validation logic added in this PR.

Minor suggestion: Consider adding that older licenses without a "plan" field are treated as valid for backward compatibility, as mentioned in the PR commit message.

Comment thread CHANGELOG.md Outdated

#### Changed

- **License-Optional Attribution Model**: React on Rails Pro now works without a license for evaluation, development, testing, and CI/CD. A paid license is only required for production deployments. Old free licenses are now treated as invalid — only paid licenses (or licenses without a `plan` field for backwards compatibility) are accepted. Documentation updated across README, LICENSE_SETUP, and CI_SETUP guides. [PR 2324](https://github.com/shakacode/react_on_rails/pull/2324) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if CHANGELOG_PRO.md exists and look at both changelog files
echo "=== Checking for CHANGELOG_PRO.md ==="
find . -name "CHANGELOG_PRO.md" -o -name "CHANGELOG.md" | head -20

echo -e "\n=== CHANGELOG.md around line 28 ==="
sed -n '20,35p' CHANGELOG.md

echo -e "\n=== Checking CHANGELOG_PRO.md if it exists ==="
if [ -f "react_on_rails_pro/CHANGELOG_PRO.md" ]; then
  echo "File found at react_on_rails_pro/CHANGELOG_PRO.md"
  head -50 react_on_rails_pro/CHANGELOG_PRO.md
else
  echo "Not found at react_on_rails_pro/CHANGELOG_PRO.md"
fi

Repository: shakacode/react_on_rails

Length of output: 1326


🏁 Script executed:

# Check the react_on_rails_pro/CHANGELOG.md file
echo "=== react_on_rails_pro/CHANGELOG.md ==="
cat react_on_rails_pro/CHANGELOG.md

Repository: shakacode/react_on_rails

Length of output: 39588


Move this entry to the Pro changelog and remove the documentation mention.

This is a Pro-specific feature ("React on Rails Pro now works without a license...") and should only appear in react_on_rails_pro/CHANGELOG.md, not in the root CHANGELOG.md. Additionally, per changelog guidelines, remove the documentation-updates clause since only user-visible changes (features, bug fixes, breaking changes, deprecations, performance improvements) belong in the changelog.

Suggested fix

Remove the entire entry from CHANGELOG.md at line 28. Then update react_on_rails_pro/CHANGELOG.md to remove the documentation mention:

- **License-Optional Attribution Model**: React on Rails Pro now works without a license for evaluation, development, testing, and CI/CD. A paid license is only required for production deployments. Added `plan` field validation to both Ruby and Node.js license validators — only `"paid"` plan (or no plan field for backwards compatibility) is accepted. Old free licenses are now treated as invalid. Documentation overhauled across README, LICENSE_SETUP, and CI_SETUP guides. [PR 2324](https://github.com/shakacode/react_on_rails/pull/2324) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
+ **License-Optional Attribution Model**: React on Rails Pro now works without a license for evaluation, development, testing, and CI/CD. A paid license is only required for production deployments. Added `plan` field validation to both Ruby and Node.js license validators — only `"paid"` plan (or no plan field for backwards compatibility) is accepted. Old free licenses are now treated as invalid. [PR 2324](https://github.com/shakacode/react_on_rails/pull/2324) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
🤖 Prompt for AI Agents
In `@CHANGELOG.md` at line 28, Remove the changelog entry that begins with
"License-Optional Attribution Model" (the entry referencing PR 2324) from the
root CHANGELOG.md and add it to the Pro changelog instead, keeping only the
user-visible description about React on Rails Pro working without a license for
evaluation/dev/testing/CI and that paid licenses are required for production
(retain the note about old free licenses being treated as
invalid/backwards-compatible plan handling), but remove the clause that mentions
documentation updates (README, LICENSE_SETUP, CI_SETUP); ensure the Pro
changelog entry references PR 2324 and the author attribution as in the
original.

Comment thread react_on_rails_pro/CHANGELOG.md Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

PR #2324 Review: License-Optional Attribution Model

Overview

This PR transforms React on Rails Pro from a "license-required" model to a "license-optional with attribution" model. The application now runs regardless of license status, displaying HTML comments indicating the license state instead of crashing.

Code Quality & Best Practices ✅

Strengths

  1. Excellent API Design

    • Clean status-based API (license_status returning :valid, :expired, :invalid, :missing)
    • Non-throwing design - never crashes the application
    • Consistent implementation across Ruby and TypeScript
    • Well-documented public methods with YARD/JSDoc comments
  2. Proper Caching Strategy

    • Single cache point (@license_status in Ruby, cachedLicenseStatus in TS)
    • Removed redundant caching of license_data since it's no longer exposed
    • Clean reset\! method for testing
  3. Security Best Practices

    • Maintains verify_signature: true in JWT.decode (with explicit comment warning against changing it)
    • Forces RS256 algorithm to prevent algorithm downgrade attacks
    • Proper error handling without exposing sensitive information
  4. Good Code Removal

    • Removed unused public methods (licensed?, license_data, isLicensed(), getLicenseData())
    • Eliminated complex grace period logic
    • Removed evaluation license concept
    • Clean diff showing meaningful reduction in complexity (1095 deletions)
  5. Test Coverage

    • Comprehensive test updates covering all license statuses
    • Proper mocking of file system and logger dependencies
    • Tests validate both happy paths and error conditions

Potential Issues & Concerns

1. ⚠️ Plan Validation Logic Could Be Clearer

Location: Both license_validator.rb:87-94 and licenseValidator.ts:97-108

The check_plan function allows licenses without a plan field for backwards compatibility. This is correct, but consider adding a comment explaining that these backwards-compatible licenses (without plan field) still undergo expiration validation in the next step.

2. 📝 Documentation: Attribution Comment Enforcement

Location: react_on_rails_pro/lib/react_on_rails_pro/utils.rb:204-219

The attribution comments are HTML comments like <\!-- Powered by React on Rails Pro (c) ShakaCode | UNLICENSED -->

Questions:

  • Are these meant to be legally binding attribution?
  • What prevents removal via minification?
  • Should there also be console.log attribution for visibility?

Recommendation: Document the legal expectations around these comments in LICENSE_SETUP.md or terms of service.

3. 🔍 Testing: Additional Error Coverage (Nice to Have)

The tests cover main scenarios well, but could add:

  • File exists but is empty
  • File exists but read permission denied
  • Malformed JWT with non-integer exp field
  • Very large license file (DoS protection)

Impact: Low priority - error handling is defensive, but additional tests would increase confidence.

Performance Considerations ✅

Excellent performance characteristics:

  • JWT decoding happens once at startup and is cached
  • Non-blocking design means no startup delays
  • pro_attribution_comment uses cached status (no repeated decoding)
  • Removed exception-based control flow

Security Considerations ✅

Strengths

  1. JWT signature verification remains mandatory with explicit warning comments
  2. Algorithm pinning (RS256 only) prevents downgrade attacks
  3. No sensitive data exposed in logs
  4. License file path is standard but not exploitable

System Clock Manipulation (Acceptable Risk)

Both implementations use local system time for expiration checking. Someone could manipulate their system clock to extend an expired license. This is acceptable for an honor-system licensing model - real enforcement would require online validation, which contradicts the offline-first design.

Documentation Quality ✅

Strengths:

  • Clear PR description with behavior comparison table
  • Comprehensive changelog entry
  • Updated LICENSE_SETUP.md with clear environment guidelines
  • Good inline comments explaining security decisions

Suggestions:

  1. Add migration guide for existing users with evaluation licenses
  2. Document that license status is cached (changes require app restart)
  3. Clarify legal standing of attribution comments

Breaking Changes - Well Handled ✅

Removed Methods

Ruby: validated_license_data\!, evaluation?, grace_days_remaining, license_data
TypeScript: getValidatedLicenseData(), isEvaluation(), getGraceDaysRemaining(), isLicensed(), getLicenseData()

These were module-internal APIs and the removal is well-documented in tests.

Recommendations

Before Merge (Optional)

  1. Add comment explaining plan validation order - Clarify that plan-less licenses still get expiration-checked
  2. Document attribution comment expectations - Add to LICENSE_SETUP.md
  3. Consider basic file system error tests - Empty file, permission denied cases

Follow-Up PRs (Nice to Have)

  1. Add comprehensive error condition test coverage
  2. Consider telemetry/monitoring for license status in production
  3. Add integration tests for full license check flow
  4. Consider adding console.log attribution in browser for visibility

Overall Assessment

Quality: 9/10 🎉

This is an excellent refactoring that significantly simplifies the licensing model while maintaining security and correctness. The status-based API is clean and consistent across languages, the security practices are solid, and the non-blocking design is appropriate for the new "license-optional" model.

Key Strengths

  • Clean, consistent API design across Ruby and TypeScript
  • Excellent security practices maintained
  • Comprehensive test coverage for main scenarios
  • Significant complexity reduction (1095 lines deleted)
  • Clear documentation and changelog

Minor Concerns

  • Attribution comment enforcement model could be clearer
  • Some edge case test coverage missing (low priority)

Verdict

Approved for merge with optional minor improvements listed above. The code is production-ready and represents a meaningful improvement over the previous implementation.

Great work on this refactoring! 👏

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Code Review: License Model Change (License-Required → License-Optional)

I've completed a thorough review of PR #2324. Overall, this is a well-implemented, high-quality change with excellent test coverage and symmetric Ruby/TypeScript implementations. Below are my findings:


Strengths

1. Excellent Code Quality

  • Symmetric implementations: Ruby and TypeScript validators are nearly identical in logic and structure
  • Clear separation of concerns: 4-step validation pipeline (load → decode → validate plan → check expiration)
  • Comprehensive error handling: All error paths log warnings and fail gracefully
  • Strong test coverage: 23 Ruby test cases + 15+ TypeScript test cases covering all edge cases

2. Robust Cryptographic Security

  • RS256 signature verification is mandatory and hardcoded (verify: true, algorithms: ['RS256'])
  • No algorithm downgrade attacks possible (algorithm is hardcoded, not parsed from token)
  • Manual expiration handling prevents JWT manipulation bypassing expiration checks
  • Clear security comments (e.g., "NEVER set to false!" on line 71 of license_validator.rb)

3. Production-Ready Error Handling

  • All error paths return graceful status symbols (:valid, :expired, :invalid, :missing)
  • No crashes or exceptions propagate to users
  • File read errors, JWT decode errors, and validation errors are all caught and logged
  • Symmetric error messages between Ruby and TypeScript implementations

4. Thoughtful Backward Compatibility

  • Licenses without 'plan' field are valid (lines 88-89 in Ruby, 99-100 in TS) - handles old paid licenses
  • Status-based API (license_status method) is a clean, future-proof design
  • HTML attribution integration is non-invasive (added to existing helper)

⚠️ Concerns & Recommendations

1. Business/Legal Risk - License Enforcement Removed

Critical Finding: This PR completely removes technical enforcement of licensing.

Before: Invalid/expired/missing licenses would block app startup (implied by description)
After: All license statuses allow app to run; enforcement is via HTML comments only

Evidence:

  • engine.rb lines 13-34: Logs warnings but never calls raise or exit
  • master.ts lines 14-37: Logs but continues process startup
  • No feature gating anywhere in codebase based on license_status

Implications:

  • License status is now informational only (honor system)
  • Users can remove HTML attribution comments easily
  • No remote revocation mechanism for compromised licenses

Recommendation:

  • ✅ Ensure legal/business team approves this model change
  • ✅ Update license terms to explicitly state attribution requirements
  • ✅ Consider adding analytics/telemetry to detect unlicensed usage (optional)

2. Missing Integration Tests

Gap: No tests verify the full integration path:

  • ✅ Unit tests for validators are excellent
  • ❌ No test that Rails engine initializer actually logs on startup
  • ❌ No test that HTML attribution comment is inserted into rendered pages
  • ❌ No test of Node renderer master process startup logging

Recommendation: Add at least one integration test:

# react_on_rails_pro/spec/integration/license_integration_spec.rb
it "logs license status on Rails startup" do
  expect(Rails.logger).to receive(:warn).with(/No license found/)
  Rails.application.initialize\!
end

it "includes attribution comment in rendered HTML" do
  visit root_path
  expect(page.html).to include("<\!-- Powered by React on Rails Pro (c) ShakaCode | UNLICENSED -->")
end

3. Security Concerns (Minor)

a) License File Could Be Committed to Git

  • Path: config/react_on_rails_pro_license.key (line 53 in Ruby, 56 in TS)
  • Risk: Developers might accidentally commit license file
  • Recommendation:
    • Ensure config/react_on_rails_pro_license.key is in .gitignore
    • Add check to generators to automatically add to .gitignore

b) Environment Variable Exposure

  • REACT_ON_RAILS_PRO_LICENSE visible in process list, logs, memory dumps
  • Recommendation: Document secure secret management in production (Vault, AWS Secrets Manager, etc.)

c) No JWT Claim Validation

  • Token signature is verified, but no validation of:
    • sub (subject/email) claim - could be arbitrary
    • iss (issuer) claim - could be forged
  • Impact: Low (signature prevents tampering, but claims aren't semantically validated)

4. Code Improvements (Minor)

a) Plan Field Type Coercion (line 88 in Ruby)

return :valid unless plan  # Treats nil, "", false identically

Issue: Uses Ruby truthy/falsy which treats empty string as truthy
Better:

return :valid if plan.nil? || plan.blank?

b) Expiration Field Type Safety

  • Ruby line 105: exp_time = license["exp"] - assumes numeric
  • No validation that exp is actually an integer (could be string "2024-01-01")
  • Recommendation: Add type check:
exp_time = license["exp"].to_i
return :invalid unless exp_time.positive?

c) Hardcoded License File Path

  • Path is not configurable: config/react_on_rails_pro_license.key
  • Issue: Breaks non-standard Rails directory structures
  • Recommendation: Add config option:
ReactOnRailsPro.configuration.license_file_path || Rails.root.join("config", "react_on_rails_pro_license.key")

d) No Clock Skew Grace Period

  • Expiration check is exact: current_time > exp_time (line 107)
  • No buffer for server clock differences
  • Recommendation: Consider 24-hour grace period for clock skew

e) Limited Logging on Success

  • When license is valid, only logs "License validated successfully"
  • Recommendation: Include license metadata:
Rails.logger.info "[React on Rails Pro] License validated for #{decoded_data['sub']} (expires #{Time.at(decoded_data['exp']).utc})"

5. Testing Gaps (Minor)

a) No TypeScript File Permission Tests

  • Ruby has file read error test (lines 154-173 in spec)
  • TypeScript only mocks file existence, not EACCES or ENOENT errors

b) No JWT Malformation Tests

  • Tests cover invalid signature
  • Missing tests for: truncated tokens, non-base64 data, garbled JSON

c) No Performance Tests

  • No verification that caching prevents repeated JWT verification
  • Could add benchmark: expect(JWT).to receive(:decode).once across multiple calls

📝 Documentation Needs

Update Required Files:

  1. CLAUDE.md: State that license enforcement is attribution-based, not technical
  2. LICENSE_SETUP.md:
    • Update to reflect license is optional for running app
    • Clarify attribution requirements
    • Add .gitignore recommendation
  3. CI_SETUP.md: Already updated (looks good!)
  4. README.md: Already updated (looks good!)

Missing Documentation:

  • No guidance on secure license storage in production
  • No explanation of what each license status means operationally
  • No troubleshooting guide for common license issues

🔍 Specific File Comments

license_validator.rb (react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb)

  • Line 71: ✅ Excellent comment about never disabling verification
  • Line 88: ⚠️ Consider using plan.nil? || plan.blank? instead of unless plan
  • Line 105: ⚠️ Add type validation for exp field
  • Line 120-122: ✅ Good centralized logging method

licenseValidator.ts (packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts)

  • Line 75: ✅ Excellent comment about algorithm downgrade attacks
  • Line 83-86: ✅ Good TypeScript error type checking
  • Line 121: ✅ Correct Unix timestamp calculation
  • Overall: Perfect symmetry with Ruby implementation

engine.rb (react_on_rails_pro/lib/react_on_rails_pro/engine.rb)

  • Lines 21-31: ✅ Clear warning messages with links to purchase
  • Line 19: Consider logging license metadata on success (email, expiry date)

utils.rb (react_on_rails_pro/lib/react_on_rails_pro/utils.rb)

  • Lines 202-219: ✅ Clean implementation of attribution comment
  • Lines 207-216: HTML comment statuses match expected behavior

🎯 Test Coverage Assessment

Ruby Tests (license_validator_spec.rb): 23 test cases - EXCELLENT

  • ✅ Valid license (ENV + file)
  • ✅ Expired license with days calculation
  • ✅ Invalid signature
  • ✅ Missing license
  • ✅ File read errors
  • ✅ Plan validation (paid/free/unknown/missing)
  • ✅ Caching behavior
  • ✅ Reset functionality

TypeScript Tests (licenseValidator.test.ts): 15+ test cases - EXCELLENT

  • ✅ All Ruby test equivalents
  • ✅ Real RSA keypair generation
  • ✅ Proper module mocking
  • ✅ Cache verification

Attribution Tests (utils_spec.rb): 4 test cases - GOOD

  • ✅ All status values tested
  • ✅ Exact HTML output verified

🚦 Final Verdict

Overall Assessment: APPROVE WITH MINOR RECOMMENDATIONS

This PR is production-ready with excellent code quality, comprehensive tests, and symmetric implementations. The core logic is sound and secure.

Required Before Merge:

  1. Verify business/legal approval for removing technical license enforcement
  2. Add integration test for HTML attribution comment rendering (5 min)
  3. Verify config/react_on_rails_pro_license.key is in .gitignore (1 min)

Recommended Improvements (Can be follow-up PRs):

  1. Add type safety for JWT exp field validation
  2. Make license file path configurable
  3. Add clock skew grace period (24 hours)
  4. Log license metadata on successful validation
  5. Add production secret management documentation

📚 References

  • Ruby validator: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
  • TypeScript validator: packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts
  • Engine integration: react_on_rails_pro/lib/react_on_rails_pro/engine.rb:13-34
  • Attribution helper: react_on_rails_pro/lib/react_on_rails_pro/utils.rb:202-219

Excellent work on this PR! The implementation is clean, well-tested, and maintains backward compatibility. The few concerns raised are minor and mostly around documentation and edge cases. 🎉

Copy link
Copy Markdown
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 `@react_on_rails_pro/LICENSE_SETUP.md`:
- Around line 166-167: Line with the bare email "[email protected]" violates
MD034; update the text in LICENSE_SETUP.md by replacing the bare email with a
mailto link (wrap the existing [email protected] in a mailto: link) so
markdownlint no longer flags it, leaving the surrounding bullet text and the
Sales line unchanged.
- Around line 166-168: The numbered list in LICENSE_SETUP.md skips from "1.
Email: [email protected]" to "3. Sales: [email protected]"; change the
"3." before the Sales entry to "2." so the sequence reads 1., 2.; update the
second list item text ("Sales:
[[email protected]](mailto:[email protected]) for pricing") accordingly.

Comment thread react_on_rails_pro/LICENSE_SETUP.md Outdated
Comment thread react_on_rails_pro/LICENSE_SETUP.md
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Comprehensive PR Review: License-Optional Attribution Model

This is a significant architectural change that fundamentally alters the licensing model from "license-required" to "license-optional with attribution." Overall, the implementation is solid and well-tested.


✅ Strengths

1. Excellent Code Quality

  • Consistent implementation across Ruby and TypeScript with parallel logic
  • Well-structured validation pipeline: load → decode → check plan → check expiration
  • Comprehensive test coverage: Both Ruby and TS tests cover all license states
  • Security-conscious: Maintains RS256 signature verification

2. Robust Error Handling

  • Non-throwing design: All methods return status symbols instead of raising exceptions
  • Graceful degradation: App continues running regardless of license state
  • Informative warnings with clear messages
  • Proper caching to avoid repeated validation

3. Good Architecture

  • Single Responsibility: LicenseValidator only determines status
  • Backwards compatibility: Old licenses without plan field still work
  • Clean API: license_status returns simple symbols
  • Separation of concerns

🔍 Areas for Improvement

1. Security Considerations

The TypeScript version is MORE secure - it explicitly enforces RS256 via algorithms array, preventing algorithm substitution attacks. Consider adding runtime assertion in Ruby.

2. Test Coverage Gap

Missing test case: Malformed JWT (not proper JWT format). Current tests cover invalid signatures but not completely malformed tokens.

3. Error Messages Could Be More Actionable

Consider adding HOW to configure the license: "Set REACT_ON_RAILS_PRO_LICENSE or create config/react_on_rails_pro_license.key"

4. Plan Field Validation

Current logic treats any plan other than "paid" as invalid. Consider centralized plan allowlist if adding enterprise/trial plans.


🔧 Performance Assessment

✅ File I/O only happens once per process
✅ RSA-256 verification expensive but cached
✅ No performance concerns


🔒 Security Assessment

Strengths: RS256 verification, no code execution, no sensitive logging
Minor: No replay protection (license reusable across apps - intentional?)


📋 Test Coverage

Well-Covered: All license states, plan validation, caching, reset
Missing: Malformed JWT, file read errors, concurrent access


🎉 Final Verdict

Overall: LGTM with minor suggestions 👍

Well-executed refactor:
✅ Maintains security
✅ Improves UX (non-blocking)
✅ Comprehensive tests
✅ Follows conventions
✅ Excellent docs

Before Merging:

  1. Run full test suite (rake)
  2. Run RuboCop (MANDATORY)
  3. Verify trailing newlines
  4. Consider test coverage gaps

Risk Level: Low 🟢

The parallel Ruby/TypeScript implementation is impressive. Great consistency!

Excellent work @AbanoubGhadban! 🚀

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Code Review: PR #2324 - Remove license requirement for React on Rails Pro

Summary

This PR implements a significant architectural change, moving from a "license-required" model (app crashes without valid license) to a "license-optional with attribution" model (app runs with HTML comments indicating license status). This is a well-executed refactor with comprehensive changes across Ruby and TypeScript codebases.

✅ Strengths

1. Consistent Architecture Across Languages

The Ruby and TypeScript implementations mirror each other perfectly:

  • Both use status-based validation (:valid, :expired, :invalid, :missing in Ruby; 'valid', 'expired', 'invalid', 'missing' in TS)
  • Both cache results appropriately
  • Both provide non-throwing helper methods (licensed?()/isLicensed(), license_data()/getLicenseData())

2. Security Best Practices Maintained

  • JWT verification still enforces algorithm: 'RS256' (prevents algorithm downgrade attacks)
  • Signature verification remains enabled (verify_expiration: false with manual expiration check)
  • Public key validation unchanged
  • Excellent security comments preserved

3. Clean API Evolution

  • Removed unused/dangerous methods (validated_license_data\!, evaluation?, grace_days_remaining)
  • New API is simpler and safer: license_status(), licensed?(), license_data()
  • No breaking changes to external consumers (Pro gem internal refactor)

4. Comprehensive Test Updates

  • Tests properly updated to match new API
  • Mock strategies changed from throwing methods to status returns
  • Test coverage appears complete

5. Good Logging Hygiene

TypeScript properly switched from console.warn/console.log to Pino logger (log.warn) as noted in PR description.

⚠️ Potential Issues & Recommendations

1. Missing TypeScript Log Import (Verify)

  • The diff shows using log.warn() in licenseValidator.ts
  • Verify import log from './shared/log.js' is added to the file
  • This might cause a compilation error if missing

2. Documentation Removal

  • react_on_rails_pro/CI_SETUP.md is completely removed (502 deletions)
  • Consider if this content should be:
    • Merged into LICENSE_SETUP.md
    • Archived for historical reference
    • Documented as a migration note for existing users with bookmarks

3. Changelog Entry Enhancement

The changelog entry should explicitly mention this is a breaking behavioral change:

  • Previous behavior: App crashes immediately without valid license
  • New behavior: App runs with warning logs and HTML attribution comments
  • This is significant for users' expectations and troubleshooting

Suggested addition to CHANGELOG_PRO.md:

#### Breaking Changes

- **[PR 2324](https://github.com/shakacode/react_on_rails/pull/2324)**: License validation no longer crashes the application. Apps now run regardless of license status with HTML attribution comments indicating license state:
  - Valid license: `<\!-- Powered by React on Rails Pro (c) ShakaCode | Licensed -->`
  - Expired license: `<\!-- Powered by React on Rails Pro (c) ShakaCode | LICENSE EXPIRED -->`
  - Invalid license: `<\!-- Powered by React on Rails Pro (c) ShakaCode | INVALID LICENSE -->`
  - No license: `<\!-- Powered by React on Rails Pro (c) ShakaCode | UNLICENSED -->`
  
  **Migration**: No code changes required. Applications will start successfully even without a license. Production deployments should still use a valid license for support and updates.

4. Method Removal Verification

The PR description states removed methods "had no production callers" - consider documenting:

  • How this was verified (code search, deprecation period, etc.)
  • Whether these were ever part of the documented public API
  • If any migration notes are needed in documentation

5. Attribution Comment Security (Business decision)

  • HTML comments are easily removed by users
  • No technical enforcement prevents comment removal
  • This is likely acceptable for a developer tool, but worth noting for the record

🔍 Testing Checklist (Per CLAUDE.md Requirements)

Before Merge:

  • Run full test suite:

    bundle exec rspec react_on_rails_pro/spec/
    pnpm --filter react-on-rails-pro-node-renderer test
  • Manual testing scenarios (from PR test plan):

    • App starts without license → Shows "UNLICENSED" comment
    • App starts with expired license → Shows "LICENSE EXPIRED" comment
    • App starts with valid license → Shows "Licensed" comment
    • App starts with invalid/corrupted license → Shows "INVALID LICENSE" comment
    • Node renderer starts without license → Logs warning, continues
  • Lint checks (MANDATORY per CLAUDE.md):

    bundle exec rubocop  # MUST pass with zero offenses
    pnpm run lint
  • Build verification:

    pnpm run build
    pnpm run yalc:publish
  • Search for removed method references:

    grep -r "validated_license_data\!" --exclude-dir=node_modules --exclude-dir=.git
    grep -r "evaluation?" --exclude-dir=node_modules --exclude-dir=.git
    grep -r "grace_days_remaining" --exclude-dir=node_modules --exclude-dir=.git
    grep -r "getValidatedLicenseData" --exclude-dir=node_modules --exclude-dir=.git
    grep -r "isEvaluation" --exclude-dir=node_modules --exclude-dir=.git
    grep -r "getGraceDaysRemaining" --exclude-dir=node_modules --exclude-dir=.git

📝 Optional Suggestions (Nice-to-Have)

1. Add Config Option for Strict Mode (Future Enhancement)

For users who want stricter behavior:

ReactOnRailsPro.configure do |config|
  config.strict_license_enforcement = true # default: false
end

2. TypeScript Type Safety (Code Quality)

Consider using const assertions:

export const LICENSE_STATUSES = ['valid', 'expired', 'invalid', 'missing'] as const;
export type LicenseStatus = typeof LICENSE_STATUSES[number];

3. Ruby Constants (Code Quality)

Consider defining status values as constants:

module LicenseStatus
  VALID = :valid
  EXPIRED = :expired
  INVALID = :invalid
  MISSING = :missing
  
  ALL = [VALID, EXPIRED, INVALID, MISSING].freeze
end

🎯 Verdict

APPROVED pending verification of the testing checklist above

This is a well-designed refactor that:

  • ✅ Improves user experience (no crashes)
  • ✅ Maintains security best practices
  • ✅ Provides consistent architecture across languages
  • ✅ Includes comprehensive test coverage
  • ✅ Will reduce support burden

The approach is sound and the implementation appears thorough. The licensing model change from "crash" to "warn with attribution" is user-friendly and appropriate for a developer tool.

Before merging, please:

  1. Complete the testing checklist above
  2. Address the missing TypeScript log import (if applicable)
  3. Consider enhancing the changelog entry as suggested
  4. Run bundle exec rubocop and ensure it passes (MANDATORY per CLAUDE.md)

Great work on maintaining code quality and security while improving the product! 🎉

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 29, 2026

Code Review: License-Optional Attribution Model

This is a major architectural change that fundamentally alters React on Rails Pro's licensing model. Overall, the implementation is well-structured and thorough, with only minor documentation improvements suggested.


✅ Strengths

  1. Excellent Code Symmetry - Ruby and TypeScript implementations mirror each other perfectly with consistent 4-state model
  2. Strong Security Practices - JWT verification never bypassed, RS256 hardcoded, proper error handling
  3. Non-Blocking Design - Application never crashes on license issues, appropriate WARN-level logging
  4. Comprehensive Test Coverage - All license states tested, edge cases covered
  5. Updated Documentation - README, LICENSE_SETUP simplified, CI_SETUP removed (correct)

⚠️ Suggestions for Improvement

🟡 Backwards Compatibility Documentation

Location: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb:83-94

The check_plan method treats missing plan field as valid for backwards compatibility. Consider adding a comment explaining when licenses without plan field were issued and why they are trusted.

🟡 Attribution Enforcement Documentation

Location: react_on_rails_pro/lib/react_on_rails_pro/utils.rb:206-221

Consider documenting in LICENSE_SETUP.md that attribution comments must not be removed by build tools, and explain this is the enforcement mechanism for unlicensed usage.

🟢 Test Performance

The tests generate new RSA key pairs for each run. Consider using before(:all) to generate once for faster execution.


📋 Pre-Merge Checklist

  • Add clarifying comments about plan field backwards compatibility
  • Document attribution comment requirements in LICENSE_SETUP.md
  • Run full test suite locally
  • Test all license states manually (valid, expired, invalid, missing)
  • Verify attribution comments render correctly in HTML

Summary

This is high-quality work with excellent test coverage and consistent implementation. Great job on the architectural consistency between Ruby and TypeScript! 🎉

Review Status: ✅ Approved with minor suggestions

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 30, 2026

PR Review: Remove license requirement for React on Rails Pro - use HTML attribution comment

Summary

This PR represents a significant architectural change to the licensing model from "license-required" to "license-optional with attribution". The implementation is generally solid, but there are several important areas that need attention.


✅ Strengths

1. Excellent Code Symmetry

The Ruby and TypeScript implementations mirror each other beautifully:

  • Both use the same status enum (:valid, :expired, :invalid, :missing)
  • Identical validation logic flow
  • Consistent error messages

This consistency will make maintenance much easier.

2. Non-Blocking Design

The refactor successfully removes all process.exit(1) and exception-throwing behavior:

  • Ruby: No longer raises ReactOnRailsPro::Error
  • TypeScript: No longer calls process.exit(1)
  • Both log warnings instead of crashing

3. Comprehensive Test Coverage

Tests cover all four license states and edge cases (missing exp field, invalid signature, etc.). The test refactoring properly removes deprecated methods.

4. Clean API Surface

The removal of unused exports simplifies the API to just what's needed.


⚠️ Issues & Concerns

1. CRITICAL: Missing license_data Cache Management in Ruby ⚠️

Location: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb:16-18

The reset! method only removes @license_status:

def reset!
  remove_instance_variable(:@license_status) if defined?(@license_status)
end

But looking at earlier commits in this PR, determine_license_status caches @license_data. If this caching is still in the code, reset! should also clear it. If it was removed, the PR description should clarify this.

Fix needed: Verify if @license_data caching was intentionally removed, and if not, add it to reset!.

2. Plan Validation Logic Missing from PR Description

Location: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb:87-94 and packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts:97-108

The code includes plan validation logic (checking plan === "paid"), but this wasn't mentioned in the PR description. This is a behavioral change for any old licenses.

Recommendation: Add a note to the PR description explaining:

  • Why plan validation was added
  • What plans are valid ("paid" only)
  • Backwards compatibility (no plan field = valid for old paid licenses)

3. Inconsistent Logging Between Ruby and TypeScript

Ruby (engine.rb:17-19):

when :expired
  Rails.logger.warn "[React on Rails Pro] License has expired. Running in unlicensed mode."

TypeScript (master.ts:25-30):

} else if (status === 'expired') {
  log.warn(
    '[React on Rails Pro] License has expired. ' +
      'Using React on Rails Pro in production without a valid license violates the license terms. ' +
      'Renew your license at https://www.shakacode.com/react-on-rails-pro/',
  );

The TypeScript version is much more explicit about license terms violations. The Ruby version just says "Running in unlicensed mode."

Recommendation: Make the warnings consistent. The TypeScript version is better because it clearly states the legal implications.

4. File Read Error Message Inconsistency

Ruby uses "Failed to read license file" while TypeScript uses "Error reading license file". Minor, but worth standardizing.


🧪 Testing Concerns

1. Missing Test: File Read Error Path

The Ruby tests don't appear to cover the rescue StandardError path in load_license_string. Add a test that triggers a file read error.

2. Missing Test: Plan Validation

Neither test suite appears to cover the plan validation logic. Add tests for:

  • License with plan: "free" → should return :invalid
  • License with plan: "paid" → should return :valid
  • License with no plan field → should return :valid (backwards compat)

📝 Documentation

1. CI_SETUP.md Deletion

The PR deletes react_on_rails_pro/CI_SETUP.md entirely (502 lines). This appears intentional since CI no longer requires licenses, but verify this information isn't needed elsewhere.


🔒 Security Considerations

Excellent Security Practices:

  1. ✅ Never disables JWT signature verification
  2. ✅ Enforces RS256 algorithm explicitly (prevents algorithm bypass attacks)
  3. ✅ Manual expiration checking (avoiding auto-validation bypass)
  4. ✅ Public key is properly separated from validation logic

🎯 Recommendations

High Priority:

  1. Clarify @license_data caching - verify if it was intentionally removed or if reset! needs updating
  2. Add plan validation to PR description under "What's Removed" or "Behavior Changes"
  3. Make warning messages consistent between Ruby and TypeScript (use TypeScript's more explicit wording)
  4. Add test coverage for plan validation logic

Medium Priority:

  1. Standardize error messages between languages
  2. Add test for file read error path in Ruby
  3. Consider adding version to attribution comment

✨ Final Thoughts

This is a well-executed refactoring that successfully transforms the licensing model. The code quality is high, with excellent test coverage and security practices.

Main Concerns:

  • Verify the @license_data caching handling
  • Add missing test coverage for plan validation
  • Improve consistency between Ruby and TypeScript warning messages

Recommendation: Approve after addressing the caching clarification and adding plan validation tests.

Great work on this significant refactor! 🎉

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jan 30, 2026

PR Review: Remove license requirement for React on Rails Pro

Overall Assessment

This is a well-executed architectural change that transforms React on Rails Pro from a "license-required" to a "license-optional with attribution" model. The implementation is thorough, consistent across both Ruby and TypeScript codebases, and includes comprehensive test coverage.

✅ Strengths

1. Architectural Consistency

  • The Ruby (license_validator.rb) and TypeScript (licenseValidator.ts) implementations mirror each other perfectly
  • Both use the same status-based approach: :valid, :expired, :invalid, :missing
  • Consistent logging patterns across both platforms

2. Security Best Practices Maintained

  • JWT signature verification remains strict (verify_signature: true / algorithms: ["RS256"])
  • Good security comments explaining why verification must never be disabled
  • Proper handling of algorithm specification to prevent downgrade attacks
  • Plan field validation prevents old "free" licenses from being used in production

3. Excellent Test Coverage

  • Comprehensive RSpec tests for all license states
  • TypeScript tests cover all status scenarios
  • Tests include edge cases (missing exp field, invalid signatures, file read errors)
  • Mock logger properly used in tests

4. Non-Breaking Change

  • Backwards compatible: licenses without plan field remain valid
  • Graceful degradation: app continues running regardless of license status
  • Users get clear warning messages with URLs to obtain licenses

5. Documentation Updates

  • README and LICENSE_SETUP.md updated to reflect new model
  • Removed obsolete CI_SETUP.md (CI no longer needs license configuration)
  • Clear changelog entries in Pro changelog

⚠️ Issues & Concerns

1. CRITICAL: Removed Public API Without Deprecation

Location: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb

The PR removes licensed? and license_data methods (Ruby) and isLicensed(), getLicenseData() (TypeScript) that were previously public exports.

Problem: If any Pro customers are calling these methods in their code, this is a breaking change without a deprecation period.

Evidence from PR description: "had no production callers" - but how was this verified?

Recommendation:

  1. Verify these methods were truly unused before merging
  2. If uncertain, consider deprecation warnings first
  3. Document in CHANGELOG as a breaking change if removing without deprecation

2. Inconsistent Error Handling Between Ruby and TypeScript

TypeScript (licenseValidator.ts:82-87):

} catch (error) {
  if (error instanceof Error) {
    logLicenseWarning(`Invalid license signature: ${error.message}. Running in unlicensed mode.`);
  }
  return undefined;
}

Problem: TypeScript only logs if error instanceof Error, potentially swallowing non-Error exceptions. Ruby catches StandardError more broadly.

Recommendation: Add a fallback for non-Error exceptions.

3. Warning Messages Should Mention Development/Testing Exemption

Location: engine.rb:25-26 and master.ts:20-24

Current warning message warns in ALL environments, including development and CI, which is confusing since the PR explicitly states these do not need licenses.

Recommendation: Adjust message based on Rails.env to clarify that dev/test environments are OK without a license, only production requires one.

4. Potential Race Condition in Caching

Location: license_validator.rb:10-13

def license_status
  return @license_status if defined?(@license_status)
  @license_status = determine_license_status
end

Problem: In a multi-threaded Rails environment, multiple threads could simultaneously call determine_license_status before @license_status is set.

Impact: Low - just inefficiency, not a correctness issue. License validation happens once at startup.

📊 Code Quality

Positive Patterns:

  • ✅ Clear separation of concerns (load → decode → validate)
  • ✅ Consistent naming across Ruby and TypeScript
  • ✅ Good use of early returns to reduce nesting
  • ✅ Descriptive error messages with actionable URLs
  • ✅ Proper use of caching to avoid repeated expensive operations

Adherence to CLAUDE.md:

  • ✅ RuboCop passing (line length issues fixed in commit 26a05c9)
  • ✅ Proper changelog updates (Pro changelog only)
  • ✅ Tests updated alongside implementation
  • ✅ No manual formatting (relies on RuboCop/Prettier)

🔒 Security Considerations

Good:

  • ✅ JWT signature verification cannot be bypassed
  • ✅ Algorithm hardcoded to prevent "none" algorithm attack
  • ✅ Plan field validation prevents unauthorized license types
  • ✅ Expiration manually checked (not auto-verified by JWT library)

📈 Performance Considerations

  • ✅ Caching prevents repeated JWT decoding
  • ✅ Validation only runs once at app startup
  • ⚠️ File I/O on every app boot (minimal impact)
  • ✅ No database queries or network calls

🧪 Testing

Coverage:

  • ✅ All four license states tested (valid, expired, invalid, missing)
  • ✅ Plan field validation tested
  • ✅ Caching behavior tested
  • ✅ Both Ruby and TypeScript have parallel test coverage

Missing Tests:

  • ⚠️ Integration test: Does HTML comment actually appear in rendered output?
  • ⚠️ Integration test: Do startup warnings appear in logs?

🎯 Recommendations Summary

Before Merge (Priority):

  1. CRITICAL: Verify licensed?, license_data, isLicensed(), getLicenseData() were truly unused by customers
  2. HIGH: Fix TypeScript error handling to catch non-Error exceptions
  3. MEDIUM: Adjust warning messages to be env-aware (production vs dev)
  4. MEDIUM: Verify documentation links are correct

Nice to Have (Post-merge):

  1. Add integration tests for HTML comment rendering
  2. Add migration guide if removing public API
  3. Consider environment-specific logging levels

🎉 Conclusion

This is a high-quality PR that makes a significant architectural improvement. The implementation is clean, well-tested, and maintains security best practices. The main concerns are around:

  1. Breaking changes without deprecation (if public API was used)
  2. Warning messages that might confuse developers
  3. Minor error handling inconsistencies

With the critical issues addressed, this PR is ready to merge. Great work on maintaining consistency across Ruby and TypeScript implementations!


Recommendation: ✅ Approve with minor changes (address items 1-4 above)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants