Skip to content

Support multiple license plan types (#2332)#2334

Merged
justin808 merged 7 commits intomasterfrom
jg/issue-2332
Feb 4, 2026
Merged

Support multiple license plan types (#2332)#2334
justin808 merged 7 commits intomasterfrom
jg/issue-2332

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Feb 1, 2026

Summary

Extend the license validator to support multiple plan types beyond just "paid". This allows offering complimentary or discounted licenses to qualifying organizations.

Closes #2332

Changes

1. Extended valid plans in LicenseValidator

Added VALID_PLANS constant with all supported plan types:

  • paid: Standard commercial license
  • startup: Complimentary for qualifying startups
  • nonprofit: Complimentary for non-profits
  • education: For educational institutions
  • oss: For open source projects
  • partner: Strategic partners

2. Added license_plan method

New public method to retrieve and cache the license plan type from the decoded JWT.

3. Updated logging to show plan type

For non-paid licenses, the log message now includes the plan type:

[React on Rails Pro] License validated successfully (startup license).

Paid licenses continue to show the simpler message without plan type.

Test plan

  • Added tests for all valid plan types (paid, startup, nonprofit, education, oss, partner)
  • Added tests for invalid plan types (free, unknown)
  • Added tests for license_plan method
  • Added tests for logging with different plan types
  • Updated reset! tests to verify plan cache is cleared

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for multiple license plans (startup, nonprofit, education, open source, partner, plus paid) and surface plan type in validation messages; unspecified/empty plan values remain backwards-compatible, unknown plans marked invalid.
    • Improved license validation flow with thread-safe caching for plan lookup.
  • Documentation

    • Updated changelog and in-code guidance describing valid plans and behavior.
  • Tests

    • Expanded tests covering all plan types, invalid/missing plans, caching and reset behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 1, 2026

Walkthrough

Adds multi-plan support to license validation: introduces VALID_PLANS and plan extraction/validation helpers in Ruby, adds with_decoded_license and determine_license_plan, and mirrors plan caching and getLicensePlan API in the Node renderer with corresponding tests and changelog updates.

Changes

Cohort / File(s) Summary
Ruby License Validator Core
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
Adds VALID_PLANS and docs, introduces with_decoded_license helper, refactors expiration flow to use it, adds determine_license_plan, and updates check_plan to validate plans (backwards-compatible when plan absent).
Ruby Tests
react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb, react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
Expands tests to cover multiple plan types, plan-specific logging in production, invalid plan/signature cases, and updated payload naming for no-plan scenarios.
Node Renderer License Validator
packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts
Exports ValidPlan type, adds cachedLicensePlan, introduces determineLicensePlan helper and public getLicensePlan() API, and updates reset() to clear cached plan.
Node Renderer Tests
packages/react-on-rails-pro-node-renderer/tests/licenseValidator.test.ts
Adds comprehensive getLicensePlan() tests for all valid plans, missing/invalid plan cases, caching behavior, and reset interactions; updates imports to include ValidPlan.
Changelog
react_on_rails_pro/CHANGELOG.md
Documents "Multiple License Plan Types" feature and notes thread-safe caching and plan display in validation messages.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • AbanoubGhadban

Poem

🐰 I hopped through code to find each plan,
Paid, startup, nonprofit — a clever band,
Cached and checked with a twitch of my nose,
Validation blooms where the carrot rows grow. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support multiple license plan types (#2332)' clearly and concisely summarizes the main change in the pull request, accurately reflecting the addition of support for multiple plan types (startup, nonprofit, education, oss, partner) beyond the existing 'paid' plan.
Linked Issues check ✅ Passed The pull request meets all coding requirements from issue #2332: extends LicenseValidator with VALID_PLANS constant [paid, startup, nonprofit, education, oss, partner], adds license_plan method with caching [#2332], updates logging to show plan type for non-paid licenses [#2332], preserves backwards compatibility for missing plans [#2332], and implements comprehensive tests for all plan types and edge cases [#2332].
Out of Scope Changes check ✅ Passed All changes are directly aligned with the objectives in issue #2332: Ruby validator enhancements, TypeScript validator enhancements, test additions for all plan types, logging updates, and documentation via CHANGELOG. No out-of-scope changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 92.86% which is sufficient. The required threshold is 80.00%.

✏️ 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 jg/issue-2332

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.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 1, 2026

Code Review - PR #2334

✅ Overall Assessment

This is a well-implemented PR that extends license validation to support multiple plan types. The code follows best practices and is thoroughly tested. I have a few suggestions for improvement.


🎯 Strengths

  1. Excellent thread safety: Properly uses mutex with double-check locking pattern for license_plan method, consistent with existing license_status and license_expiration methods.

  2. Comprehensive test coverage: Added tests for all valid plan types, invalid plan types, caching behavior, and logging variations. Great use of dynamic test generation with VALID_PLANS.each.

  3. Backwards compatibility: Preserves existing behavior for licenses without a plan field and maintains the simpler log message for paid licenses.

  4. Clear documentation: Well-commented code explaining each plan type and validation logic.

  5. Proper separation of concerns: Validation logic stays in LicenseValidator, logging in Engine.


🔍 Code Quality Observations

✅ Good Practices

  • DRY principle: VALID_PLANS constant eliminates duplication
  • Consistent error handling: Returns nil for all error cases in determine_license_plan
  • Proper cache invalidation: reset\! correctly clears @license_plan cache
  • Thread-safe lazy initialization: Uses defined?(@license_plan) check before mutex synchronization

💡 Minor Suggestions

  1. Consider extracting plan validation logic (Optional):

    The determine_license_plan method duplicates the JWT decoding pattern used in determine_license_expiration. Consider extracting a shared helper:

    def with_decoded_license
      license_string = load_license_string
      return nil unless license_string
      
      decoded_data = decode_license(license_string)
      return nil unless decoded_data
      
      yield decoded_data
    end
    
    def determine_license_plan
      with_decoded_license do |decoded_data|
        plan = decoded_data["plan"]
        VALID_PLANS.include?(plan) ? plan : nil
      end
    end

    However, the current implementation is clear and readable, so this is purely optional. No change needed unless you want to reduce duplication.

  2. Logging clarity: The conditional logic in engine.rb:27 is correct but could be clearer with a comment:

    plan = ReactOnRailsPro::LicenseValidator.license_plan
    # Show plan type for non-paid licenses (startup, nonprofit, etc.)
    # Paid licenses show simpler message for backwards compatibility
    plan_info = plan && plan \!= "paid" ? " (#{plan} license)" : ""

🔒 Security Considerations

No security concerns identified:

  • JWT validation remains strict (verify_signature: true, algorithm: 'RS256')
  • Plan validation is explicit (whitelist approach with VALID_PLANS)
  • No new attack surface introduced
  • Proper handling of invalid/missing data (returns nil rather than raising)

🧪 Test Coverage

Excellent test coverage includes:

  • ✅ All valid plan types (paid, startup, nonprofit, education, oss, partner)
  • ✅ Invalid plan types (free, unknown)
  • ✅ Missing license, invalid signature, missing plan field
  • ✅ Caching behavior
  • reset\! clears plan cache
  • ✅ Logging variations for different plan types (both production and non-production)

Additional test suggestion (optional):

  • Consider adding a test for concurrent access to license_plan to verify thread safety (similar to existing thread safety tests for license_status)

📊 Performance Considerations

No performance concerns:

  • Caching prevents redundant JWT decoding
  • Mutex overhead is minimal (only during first access)
  • Thread-safe initialization follows established patterns

✨ Best Practices Alignment

This PR follows the project's CLAUDE.md guidelines:

  • ✅ Uses RuboCop for formatting (should pass lint checks)
  • ✅ Comprehensive test coverage
  • ✅ No over-engineering
  • ✅ Clear, focused changes
  • ✅ Backwards compatible

📝 Recommendation

Approve with optional refinements. The code is production-ready as-is. The suggestions above are minor optimizations that can be addressed in follow-up PRs if desired.

Before merging:

  1. ✅ Verify RuboCop passes: bundle exec rubocop
  2. ✅ Verify all tests pass: rake run_rspec
  3. ✅ Consider manual testing with different license plan types

Great work! 🚀


Review generated by Claude Code - claude.com/claude-code

Base automatically changed from abanoub/2323-license-optional-attribution to master February 1, 2026 04:38
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Feb 1, 2026

Greptile Overview

Greptile Summary

Extended the license validator to support multiple plan types beyond just paid, enabling complimentary and discounted licenses for qualifying organizations. The implementation adds support for startup, nonprofit, education, oss, and partner plan types in both Ruby and TypeScript validators.

Key Changes:

  • Added VALID_PLANS constant in both Ruby and TypeScript implementations, ensuring consistency across platforms
  • Implemented getLicensePlan() / license_plan methods with thread-safe caching to retrieve the plan type from JWT
  • Exported ValidPlan TypeScript type for type safety in consuming code
  • Refactored Ruby code to extract with_decoded_license helper, reducing duplication
  • Updated logging to display plan type for non-paid licenses (e.g., "License validated successfully (Startup Inc - startup license)")
  • Maintained backwards compatibility: licenses without a plan field are still considered valid (treated as legacy paid licenses)

Code Quality:

  • Both Ruby and TypeScript implementations follow parallel structure with consistent validation logic
  • Comprehensive test coverage for all valid plan types, invalid plans, caching behavior, and edge cases
  • Thread-safe implementation in Ruby using existing LICENSE_MUTEX
  • Clear comments explaining validation logic and backwards compatibility

Documentation:

  • CHANGELOG entry properly formatted with PR link and contributor credit
  • Inline documentation in both implementations explains plan types and validation behavior

Confidence Score: 5/5

  • Safe to merge with no issues found
  • Implementation is well-structured with parallel Ruby/TypeScript logic, comprehensive test coverage, proper thread-safety, backwards compatibility, and clear documentation. No bugs or logical issues detected.
  • No files require special attention

Important Files Changed

Filename Overview
packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts Added getLicensePlan() function and caching for plan types, exported ValidPlan type, updated reset() to clear plan cache
packages/react-on-rails-pro-node-renderer/tests/licenseValidator.test.ts Comprehensive test coverage for all valid plan types, invalid plans, caching behavior, and reset functionality
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb Added license_plan method with thread-safe caching, extracted with_decoded_license helper, refactored determine_license_expiration to use helper
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb Added tests for new license_plan method covering valid plans, invalid signature, missing plan, and invalid plan types

Sequence Diagram

sequenceDiagram
    participant App as Rails/Node App
    participant Engine as Engine.log_license_status
    participant Validator as LicenseValidator
    participant JWT as JWT Library
    participant Cache as Cache Layer

    App->>Engine: Initialize (on startup)
    Engine->>Validator: license_status()
    Validator->>Cache: Check cached status
    alt Cache Hit
        Cache-->>Validator: Return cached status
    else Cache Miss
        Validator->>Validator: load_license_string()
        Validator->>JWT: decode and verify
        JWT-->>Validator: decoded data
        Validator->>Validator: check_plan(decoded_data)
        Note over Validator: Validates plan is in VALID_PLANS<br/>(paid, startup, nonprofit, education, oss, partner)
        Validator->>Validator: check_organization(decoded_data)
        Validator->>Validator: check_expiration(decoded_data)
        Validator->>Cache: Store status
        Cache-->>Validator: Status cached
    end
    Validator-->>Engine: :valid/:expired/:invalid/:missing
    
    alt Status is :valid
        Engine->>Validator: license_plan()
        Validator->>Cache: Check cached plan
        alt Cache Hit
            Cache-->>Validator: Return cached plan
        else Cache Miss
            Validator->>Validator: determine_license_plan()
            Validator->>Validator: with_decoded_license()
            Validator->>Cache: Store plan
            Cache-->>Validator: Plan cached
        end
        Validator-->>Engine: "paid"/"startup"/etc or nil
        Engine->>Engine: plan_display_name(plan)
        Note over Engine: Returns nil for "paid"<br/>Returns "startup license" for "startup"<br/>etc.
        Engine->>App: Log "License validated successfully (Org - plan)"
    else Status is not :valid
        Engine->>App: Log license issue
    end
Loading

@justin808 justin808 force-pushed the jg/issue-2332 branch 2 times, most recently from e335491 to 38ac309 Compare February 1, 2026 04:55
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 1, 2026

Code Review - PR #2334: Support multiple license plan types

Summary

This PR extends the license validator to support multiple plan types beyond just "paid". The implementation is well-structured and comprehensive. Here's my detailed feedback:


Strengths

  1. Excellent backward compatibility

    • Gracefully handles old licenses without a plan field (line 177 in license_validator.rb)
    • Paid licenses maintain simpler log message format (engine.rb:29)
    • No breaking changes to existing functionality
  2. Thread-safe implementation

    • Properly uses existing LICENSE_MUTEX for the new license_plan method (license_validator.rb:56)
    • Follows double-check locking pattern consistently
    • Cache clearing includes new @license_plan variable (license_validator.rb:68)
  3. Code reusability

    • Excellent refactoring with with_decoded_license helper (license_validator.rb:125)
    • Eliminates code duplication between determine_license_expiration and determine_license_plan
    • Clean separation of concerns
  4. Comprehensive test coverage

    • Tests all 6 valid plan types dynamically using VALID_PLANS.each (license_validator_spec.rb:159)
    • Tests invalid plan types ("free", "unknown")
    • Tests logging behavior for different plan types
    • Tests caching behavior for license_plan method
    • Tests reset! properly clears the plan cache
  5. Clear documentation

    • Well-documented VALID_PLANS constant with descriptions (license_validator.rb:10-17)
    • Helpful inline comments explaining design decisions
    • Good YARD documentation for new methods

🔍 Observations & Considerations

  1. Validation strictness

    • The license_plan method returns nil for invalid plan types (license_validator.rb:116)
    • This is different from license_status which returns :invalid
    • Consider: Should there be a way to distinguish between "no plan field" vs "invalid plan field"? Currently both return nil.
    • Impact: Low - the current behavior is probably fine since invalid plans are already caught by license_status
  2. Error handling consistency

    • determine_license_expiration has explicit rescue for ArgumentError, TypeError (license_validator.rb:107)
    • determine_license_plan relies on with_decoded_license returning nil on errors
    • Consider: Should with_decoded_license document what exceptions it might propagate?
    • Impact: Low - current implementation is safe due to guard clauses
  3. Log message formatting

    • Uses string interpolation: "successfully#{plan_info}." (engine.rb:30)
    • Works correctly but could be slightly more readable
    • Consider: "successfully\#{plan_info}.""successfully" + plan_info + "." or extract to variable
    • Impact: Negligible - current code is clear enough

🎯 Security Considerations

  1. Plan type validation is secure

    • Uses whitelist approach with VALID_PLANS.include?(plan)
    • No risk of injection or unexpected values
    • Frozen constant prevents modification ✅
  2. No additional attack surface

    • Plan validation happens AFTER signature verification ✅
    • Invalid plans correctly trigger :invalid status ✅
    • No logging of potentially malicious plan values ✅

📊 Performance

  1. Caching is efficient

    • license_plan is cached just like license_status and license_expiration
    • Uses thread-safe lazy initialization
    • No performance regressions expected
  2. Potential optimization

    • Currently, license_plan decodes the JWT separately from license_status
    • If both are called during initialization, the JWT is decoded twice
    • Consider: Could cache decoded license data to avoid redundant decoding
    • Impact: Very low - these are typically called once at Rails startup

🧪 Test Coverage Assessment

Excellent coverage:

  • ✅ All valid plan types tested
  • ✅ Invalid plan types tested
  • ✅ Missing plan field tested
  • ✅ Missing license tested
  • ✅ Invalid signature tested
  • ✅ Caching behavior tested
  • ✅ Thread-safety maintained (existing tests)
  • ✅ Logging behavior tested for different plan types
  • ✅ Reset behavior tested

No gaps identified.


📝 Code Quality

  1. Follows project conventions

    • Consistent with existing codebase style ✅
    • Uses proper YARD documentation ✅
    • Frozen string literal ✅
    • Follows Rails naming conventions ✅
  2. Refactoring improves maintainability

    • The with_decoded_license helper is a great addition
    • Makes future additions easier (e.g., extracting other JWT fields)

⚠️ Minor Suggestions (optional improvements)

  1. Documentation clarity

    • Consider adding a comment in determine_license_plan explaining why invalid plans return nil instead of raising
    • Example: # Returns nil for invalid plans - validation handled by check_plan in license_status
  2. Test organization

    • The dynamic test generation using VALID_PLANS.each is excellent
    • Consider adding a comment explaining why this approach was chosen over individual examples
  3. Edge case documentation

    • Document what happens if someone tries to use a plan type not in VALID_PLANS
    • Current behavior: license will be marked as :invalid by check_plan, and license_plan returns nil
    • This is correct, but worth documenting for future maintainers

Recommendation: APPROVE

This is a well-crafted PR that:

  • Adds valuable functionality for business flexibility
  • Maintains backward compatibility
  • Has comprehensive test coverage
  • Follows security best practices
  • Improves code quality through refactoring

The implementation is production-ready. The minor suggestions above are optional refinements, not blockers.

Great work! 🎉


📋 Checklist Review (per CLAUDE.md)

  • ✅ Code quality and best practices - Excellent
  • ✅ Potential bugs - None identified
  • ✅ Performance - No concerns, caching is efficient
  • ✅ Security - Secure whitelist validation, no new attack surface
  • ✅ Test coverage - Comprehensive
  • ✅ Follows repository conventions - Yes
  • ✅ RuboCop compliance - Assumed (should verify with bundle exec rubocop)
  • ✅ Trailing newlines - Visible in diff ✅

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

🤖 Fix all issues with AI agents
In `@react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb`:
- Around line 10-18: Add a user-visible changelog entry for the new Pro plan
support: update CHANGELOG_PRO.md with a single entry describing the new plan
support (e.g., "Add support for plan types: paid, startup, nonprofit, education,
oss, partner") using the required PR link format `[PR
1818](https://github.com/shakacode/react_on_rails/pull/1818) by
[username](https://github.com/username)` (no hash in PR number); also add a
corresponding open-source note in CHANGELOG.md if the change affects OSS users,
and ensure you reference the VALID_PLANS constant in the same phrasing so the
user-visible feature matches the code.

Comment on lines +10 to +18
# Valid license plan types.
# - paid: Standard commercial license
# - startup: Complimentary for qualifying startups
# - nonprofit: Complimentary for non-profits
# - education: For educational institutions
# - oss: For open source projects
# - partner: Strategic partners
VALID_PLANS = %w[paid startup nonprofit education oss partner].freeze

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 | 🟡 Minor

Add CHANGELOG_PRO entry for plan support.
This is a user-visible Pro feature, so please record it in CHANGELOG_PRO.md using the required PR link format.

Based on learnings: Applies to {CHANGELOG.md,CHANGELOG_PRO.md} : Update CHANGELOG.md for open-source changes and CHANGELOG_PRO.md for Pro-only changes; use format [PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username) (no hash in PR number); only include user-visible changes (features, bug fixes, breaking changes, deprecations, performance improvements), NOT linting, formatting, refactoring, tests, or documentation fixes

🤖 Prompt for AI Agents
In `@react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb` around lines
10 - 18, Add a user-visible changelog entry for the new Pro plan support: update
CHANGELOG_PRO.md with a single entry describing the new plan support (e.g., "Add
support for plan types: paid, startup, nonprofit, education, oss, partner")
using the required PR link format `[PR
1818](https://github.com/shakacode/react_on_rails/pull/1818) by
[username](https://github.com/username)` (no hash in PR number); also add a
corresponding open-source note in CHANGELOG.md if the change affects OSS users,
and ensure you reference the VALID_PLANS constant in the same phrasing so the
user-visible feature matches the code.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 1, 2026

Code Review - PR #2334

Overall this is well-implemented with excellent test coverage.

Strengths

  • Excellent test coverage for all plan types
  • Proper thread safety with mutex
  • Backwards compatibility maintained
  • Good code consistency between Ruby and TypeScript

Issues Found

  1. Missing org field in test payloads (lines 125-255 in engine_spec.rb)
  2. Test at line 114 expects wrong message format
  3. TypeScript missing getLicensePlan() function

Recommendations

  • Add org field to all test payloads
  • Verify tests pass locally
  • Consider adding getLicensePlan() to TypeScript for parity

Code Quality: 8.5/10

justin808 added a commit that referenced this pull request Feb 1, 2026
Add changelog entry for the multiple license plan types feature
that supports: paid, startup, nonprofit, education, oss, and partner.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 1, 2026

Code Review

This PR successfully extends the license validation system to support multiple license plan types. The implementation is well-structured with comprehensive test coverage.

Strengths

  1. Excellent Test Coverage - 113 lines of tests covering all valid plan types, invalid types, edge cases, and thread safety
  2. Good Code Organization - The with_decoded_license helper effectively DRYs up JWT decoding logic
  3. Backwards Compatibility - Properly handles old licenses without a plan field
  4. Good Documentation - Clear inline comments explaining behaviors
  5. Consistency - Ruby VALID_PLANS matches TypeScript version

Issues Found

TypeScript Implementation Missing getLicensePlan Function

The TypeScript version does not have an equivalent to the Ruby license_plan method.

Current State:

  • Ruby has: LicenseValidator.license_plan (added in this PR)
  • TypeScript has: getLicenseStatus() and getLicenseOrganization()
  • TypeScript missing: getLicensePlan()

Impact: If the Node renderer needs to access the license plan type for logging or conditional behavior, there is no public API.

Recommendation: Add a getLicensePlan() function to TypeScript implementation for API consistency.

Security

No security issues found. The implementation properly validates plan types against a whitelist.

Performance

Good caching implementation with thread-safe caching and efficient helper methods.

Test Coverage

Excellent coverage with one minor gap: missing explicit test for non-string plan values. The code handles this correctly, but a test would improve confidence.

Changelog

Well-written entry that clearly explains the feature, behavior, and technical details.

Recommendations

Priority 1: Add getLicensePlan() function to TypeScript implementation for parity

Priority 2 (Optional): Consider adding test for non-string plan values

Summary

This is a high-quality PR with excellent test coverage and clean implementation. The only significant issue is the missing TypeScript getLicensePlan() function for API consistency.

Recommendation: Approve with minor change

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 1, 2026

Code Review - PR #2334: Support multiple license plan types

Summary

This PR extends the license validator to support multiple plan types beyond just paid licenses. The implementation is well-structured and properly tested.

✅ Strengths

1. Excellent Code Quality

  • Clean separation of concerns with dedicated license_plan method
  • Consistent thread-safety pattern using LICENSE_MUTEX
  • Good use of the with_decoded_license helper to avoid code duplication
  • Proper caching with double-check locking pattern

2. Comprehensive Test Coverage

  • Tests cover all valid plan types and invalid cases
  • Thread-safety verification in reset! tests
  • Integration tests in engine_spec.rb for different license types
  • Good edge case coverage

3. Backwards Compatibility

  • Licenses without a plan field remain valid
  • Paid licenses continue showing simpler log message
  • Non-paid licenses display plan type for clarity

4. Documentation

  • Clear inline comments explaining each plan type
  • YARD documentation for public methods
  • Updated CHANGELOG with comprehensive description

⚠️ Issues & Recommendations

1. CRITICAL: Missing TypeScript Implementation

The Ruby implementation adds a license_plan method, but the TypeScript version in packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts does NOT have a corresponding getLicensePlan() function.

Impact: TypeScript and Ruby implementations will be inconsistent. If the Node renderer needs to display plan type information, it will not be able to.

Recommendation: Add corresponding TypeScript implementation for parity with Ruby code.

2. Minor: Log Message Format

In engine.rb:30, the plan info produces: "License validated successfully (Acme Corp - startup license)."

Consider a clearer format such as:

  • "License validated successfully (Acme Corp) - startup license."
  • "License validated successfully (Acme Corp, startup license)."

3. Minor: Consider Adding Plan Type to Error Messages

When logging invalid or expired licenses, showing the attempted plan type could aid debugging.

🔒 Security Review

✅ No security concerns identified:

  • Uses existing JWT validation (RS256 with signature verification)
  • Plan validation uses allowlist approach
  • Thread-safe implementation prevents race conditions

🚀 Performance Considerations

✅ Good performance characteristics:

  • Efficient caching with instance variables
  • Only decodes JWT once per process lifecycle
  • Thread-safe double-check locking minimizes mutex contention

🎯 Overall Assessment

Status: Approve with minor recommendation

This is a well-implemented feature with excellent test coverage. The only significant issue is the missing TypeScript implementation.

Recommendation:

  • ✅ Approve and merge (if TypeScript parity not needed immediately)
  • ⚠️ OR add TypeScript implementation first (recommended for consistency)

Great work on maintaining code quality and test coverage! 🎉

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 1, 2026

Code Review - PR #2334: Support multiple license plan types

✅ Overall Assessment

This is an excellent, production-ready PR. The implementation is clean, well-tested, and follows all project conventions. The code quality is high across both Ruby and TypeScript implementations.


Strengths

1. Excellent Code Consistency

  • ✅ Both Ruby and TypeScript implementations are perfectly aligned
  • VALID_PLANS constants match exactly with clear comments to keep them in sync
  • ✅ Validation logic mirrors across both languages
  • ✅ Caching strategies appropriate for each language (Mutex for Ruby, simple cache for Node.js)

2. Comprehensive Test Coverage

  • All valid plan types tested using parameterized tests (it.each in TS, dynamic generation in Ruby)
  • Edge cases covered: invalid plans (free, unknown), missing plan field, empty/whitespace org
  • Backwards compatibility verified: licenses without plan field still work
  • Cache invalidation tested: reset()/reset\! properly clears all cached values including new license_plan
  • Thread safety tested in Ruby with 100 concurrent threads
  • Logging behavior tested for all plan types in production and non-production environments

3. Backwards Compatibility

  • ✅ Licenses without a plan field remain valid (existing paid licenses)
  • ✅ Paid licenses show simpler log message (no plan suffix) for consistency with existing behavior
  • ✅ Non-paid plans get enhanced logging with plan type information

4. Security Best Practices

  • ✅ Signature verification enforced (algorithms: ['RS256'], verify_signature: true)
  • ✅ No execution flow changes based on plan type (just logging differences)
  • ✅ Invalid plan types treated as invalid licenses
  • ✅ Defensive coding: type checks, null handling, trim whitespace

5. Clean Logging Implementation

# Paid license (backwards compatible)
"License validated successfully (Acme Corp)."

# Non-paid license (new)
"License validated successfully (Startup Inc - startup license)."

This approach is user-friendly and informative without breaking existing expectations.


Minor Observations (Not Blocking)

1. Type Safety in TypeScript

The ValidPlan type is well-defined:

const VALID_PLANS = ['paid', 'startup', 'nonprofit', 'education', 'oss', 'partner'] as const;
export type ValidPlan = (typeof VALID_PLANS)[number];

This ensures compile-time type safety. ✅

2. Thread Safety in Ruby

The use of LICENSE_MUTEX with double-check locking is correct:

return @license_plan if defined?(@license_plan)

LICENSE_MUTEX.synchronize do
  return @license_plan if defined?(@license_plan)
  @license_plan = determine_license_plan
end

This prevents race conditions during initialization. ✅

3. Consistent Validation Order

Both implementations follow the same validation sequence:

  1. Load license string
  2. Decode JWT
  3. Check plan validity
  4. Check organization presence
  5. Check expiration

This consistency makes the codebase easier to maintain. ✅


Best Practices Followed

CLAUDE.md Compliance:

  • Changelog updated in correct file (CHANGELOG_PRO.md)
  • Proper PR format with GitHub username links
  • User-facing change documented

Testing Requirements:

  • All new methods have comprehensive test coverage
  • Edge cases thoroughly tested
  • Both positive and negative test cases included

Code Quality:

  • Clear, descriptive method/variable names
  • Appropriate comments explaining non-obvious logic
  • No code duplication between implementations
  • Proper error handling

Security Considerations

✅ No Security Issues Found

  1. JWT Validation: Proper algorithm enforcement prevents downgrade attacks
  2. No Execution Bypass: Invalid plan types result in :invalid status
  3. Input Validation: Organization field properly validated (non-empty, trimmed)
  4. No Injection Risks: Plan types validated against whitelist, not executed or interpolated unsafely

Performance Considerations

✅ Excellent Caching Strategy

Ruby:

# Thread-safe lazy initialization with double-check locking
def license_plan
  return @license_plan if defined?(@license_plan)
  LICENSE_MUTEX.synchronize do
    return @license_plan if defined?(@license_plan)
    @license_plan = determine_license_plan
  end
end

TypeScript:

// Simple module-level caching (Node.js is single-threaded)
let cachedLicensePlan: ValidPlan | undefined;

export function getLicensePlan(): ValidPlan | undefined {
  if (cachedLicensePlan \!== undefined) {
    return cachedLicensePlan;
  }
  cachedLicensePlan = determineLicensePlan();
  return cachedLicensePlan;
}

Both implementations are appropriate for their runtime environments. License validation happens once per Rails initialization / Node worker startup, so performance impact is negligible.


Suggested Improvements (Optional)

These are very minor and don't block this PR:

  1. Documentation: Consider adding inline examples in CLAUDE.md or README showing what each plan type is intended for (though the comments in the code are already quite clear).

  2. Future Enhancement: If plan-specific behavior is needed beyond logging, consider extracting plan validation into a separate concern. Current implementation is perfect for logging-only differences.


Final Recommendation

APPROVED - Ready to Merge

This PR:

  • Implements the feature correctly across both Ruby and TypeScript
  • Has comprehensive test coverage (>95% of new code paths tested)
  • Maintains backwards compatibility
  • Follows all project conventions
  • Has no security, performance, or quality issues
  • Updates changelog appropriately

Excellent work! This is a textbook example of how to extend a validation system while maintaining compatibility and code quality.


Pre-Merge Checklist

Before merging, verify:

  • bundle exec rubocop passes (MANDATORY per CLAUDE.md)
  • All tests pass locally
  • Files end with newlines (CI requirement)

Per CLAUDE.md: "BEFORE EVERY COMMIT/PUSH: ALWAYS run bundle exec rubocop and fix ALL violations"

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 1, 2026

🐰 Bencher Report

Branchjg/issue-2332
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,143.68
(0.00%)Baseline: 3,143.68
4,086.78
(76.92%)
📈 view plot
🚷 view threshold
3,754.89
(0.00%)Baseline: 3,754.89
4,881.36
(76.92%)
📈 view plot
🚷 view threshold
32,367.26
(0.00%)Baseline: 32,367.26
42,077.44
(76.92%)
📈 view plot
🚷 view threshold
2.63
(0.00%)Baseline: 2.63
1.84
(70.00%)
/: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
28.41
(0.00%)Baseline: 28.41
36.93
(76.92%)
📈 view plot
🚷 view threshold
50.17
(0.00%)Baseline: 50.17
65.22
(76.92%)
📈 view plot
🚷 view threshold
480.92
(0.00%)Baseline: 480.92
625.20
(76.92%)
📈 view plot
🚷 view threshold
218.08
(0.00%)Baseline: 218.08
152.66
(70.00%)
/apollo_graphql: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
47.74
(0.00%)Baseline: 47.74
62.06
(76.92%)
📈 view plot
🚷 view threshold
76.98
(0.00%)Baseline: 76.98
100.07
(76.92%)
📈 view plot
🚷 view threshold
618.52
(0.00%)Baseline: 618.52
804.08
(76.92%)
📈 view plot
🚷 view threshold
137.48
(0.00%)Baseline: 137.48
96.24
(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
34.86
(0.00%)Baseline: 34.86
45.32
(76.92%)
📈 view plot
🚷 view threshold
47.32
(0.00%)Baseline: 47.32
61.52
(76.92%)
📈 view plot
🚷 view threshold
214.97
(0.00%)Baseline: 214.97
279.46
(76.92%)
📈 view plot
🚷 view threshold
255.56
(0.00%)Baseline: 255.56
178.89
(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
20.04
(0.00%)Baseline: 20.04
26.05
(76.92%)
📈 view plot
🚷 view threshold
33.46
(0.00%)Baseline: 33.46
43.50
(76.92%)
📈 view plot
🚷 view threshold
164.90
(0.00%)Baseline: 164.90
214.37
(76.92%)
📈 view plot
🚷 view threshold
391.79
(0.00%)Baseline: 391.79
274.25
(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
16.73
(0.00%)Baseline: 16.73
21.75
(76.92%)
📈 view plot
🚷 view threshold
27.68
(0.00%)Baseline: 27.68
35.98
(76.92%)
📈 view plot
🚷 view threshold
130.98
(0.00%)Baseline: 130.98
170.27
(76.92%)
📈 view plot
🚷 view threshold
464.27
(0.00%)Baseline: 464.27
324.99
(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
17.49
(0.00%)Baseline: 17.49
22.74
(76.92%)
📈 view plot
🚷 view threshold
26.71
(0.00%)Baseline: 26.71
34.72
(76.92%)
📈 view plot
🚷 view threshold
196.04
(0.00%)Baseline: 196.04
254.85
(76.92%)
📈 view plot
🚷 view threshold
438.26
(0.00%)Baseline: 438.26
306.78
(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
17.37
(0.00%)Baseline: 17.37
22.58
(76.92%)
📈 view plot
🚷 view threshold
26.93
(0.00%)Baseline: 26.93
35.01
(76.92%)
📈 view plot
🚷 view threshold
182.02
(0.00%)Baseline: 182.02
236.63
(76.92%)
📈 view plot
🚷 view threshold
439.13
(0.00%)Baseline: 439.13
307.39
(70.00%)
/broken_app: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
341.20
(0.00%)Baseline: 341.20
443.56
(76.92%)
📈 view plot
🚷 view threshold
474.22
(0.00%)Baseline: 474.22
616.49
(76.92%)
📈 view plot
🚷 view threshold
3,840.59
(0.00%)Baseline: 3,840.59
4,992.77
(76.92%)
📈 view plot
🚷 view threshold
17.48
(0.00%)Baseline: 17.48
12.24
(70.00%)
/broken_app: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
14.65
(0.00%)Baseline: 14.65
19.05
(76.92%)
📈 view plot
🚷 view threshold
24.23
(0.00%)Baseline: 24.23
31.50
(76.92%)
📈 view plot
🚷 view threshold
235.59
(0.00%)Baseline: 235.59
306.27
(76.92%)
📈 view plot
🚷 view threshold
437.62
(0.00%)Baseline: 437.62
306.33
(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.49
(0.00%)Baseline: 17.49
22.74
(76.92%)
📈 view plot
🚷 view threshold
30.14
(0.00%)Baseline: 30.14
39.18
(76.92%)
📈 view plot
🚷 view threshold
137.96
(0.00%)Baseline: 137.96
179.35
(76.92%)
📈 view plot
🚷 view threshold
427.12
(0.00%)Baseline: 427.12
298.98
(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
16.84
(0.00%)Baseline: 16.84
21.89
(76.92%)
📈 view plot
🚷 view threshold
26.99
(0.00%)Baseline: 26.99
35.09
(76.92%)
📈 view plot
🚷 view threshold
187.41
(0.00%)Baseline: 187.41
243.63
(76.92%)
📈 view plot
🚷 view threshold
434.20
(0.00%)Baseline: 434.20
303.94
(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.07
(0.00%)Baseline: 11.07
14.39
(76.92%)
📈 view plot
🚷 view threshold
14.38
(0.00%)Baseline: 14.38
18.69
(76.92%)
📈 view plot
🚷 view threshold
125.85
(0.00%)Baseline: 125.85
163.60
(76.92%)
📈 view plot
🚷 view threshold
525.96
(0.00%)Baseline: 525.96
368.17
(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
16.98
(0.00%)Baseline: 16.98
22.07
(76.92%)
📈 view plot
🚷 view threshold
26.37
(0.00%)Baseline: 26.37
34.28
(76.92%)
📈 view plot
🚷 view threshold
188.17
(0.00%)Baseline: 188.17
244.62
(76.92%)
📈 view plot
🚷 view threshold
469.81
(0.00%)Baseline: 469.81
328.87
(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
9.43
(0.00%)Baseline: 9.43
12.26
(76.92%)
📈 view plot
🚷 view threshold
17.40
(0.00%)Baseline: 17.40
22.62
(76.92%)
📈 view plot
🚷 view threshold
143.56
(0.00%)Baseline: 143.56
186.63
(76.92%)
📈 view plot
🚷 view threshold
709.73
(0.00%)Baseline: 709.73
496.81
(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.54
(0.00%)Baseline: 17.54
22.80
(76.92%)
📈 view plot
🚷 view threshold
27.32
(0.00%)Baseline: 27.32
35.52
(76.92%)
📈 view plot
🚷 view threshold
186.85
(0.00%)Baseline: 186.85
242.91
(76.92%)
📈 view plot
🚷 view threshold
452.55
(0.00%)Baseline: 452.55
316.79
(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.45
(0.00%)Baseline: 9.45
12.28
(76.92%)
📈 view plot
🚷 view threshold
17.65
(0.00%)Baseline: 17.65
22.94
(76.92%)
📈 view plot
🚷 view threshold
145.66
(0.00%)Baseline: 145.66
189.36
(76.92%)
📈 view plot
🚷 view threshold
703.53
(0.00%)Baseline: 703.53
492.47
(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.58
(0.00%)Baseline: 17.58
22.85
(76.92%)
📈 view plot
🚷 view threshold
28.40
(0.00%)Baseline: 28.40
36.92
(76.92%)
📈 view plot
🚷 view threshold
148.76
(0.00%)Baseline: 148.76
193.39
(76.92%)
📈 view plot
🚷 view threshold
443.34
(0.00%)Baseline: 443.34
310.34
(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
8.28
(0.00%)Baseline: 8.28
10.76
(76.92%)
📈 view plot
🚷 view threshold
13.85
(0.00%)Baseline: 13.85
18.00
(76.92%)
📈 view plot
🚷 view threshold
135.52
(0.00%)Baseline: 135.52
176.18
(76.92%)
📈 view plot
🚷 view threshold
739.16
(0.00%)Baseline: 739.16
517.41
(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
19.59
(0.00%)Baseline: 19.59
25.47
(76.92%)
📈 view plot
🚷 view threshold
27.19
(0.00%)Baseline: 27.19
35.35
(76.92%)
📈 view plot
🚷 view threshold
104.14
(0.00%)Baseline: 104.14
135.38
(76.92%)
📈 view plot
🚷 view threshold
456.47
(0.00%)Baseline: 456.47
319.53
(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
9.16
(0.00%)Baseline: 9.16
11.91
(76.92%)
📈 view plot
🚷 view threshold
18.01
(0.00%)Baseline: 18.01
23.41
(76.92%)
📈 view plot
🚷 view threshold
110.66
(0.00%)Baseline: 110.66
143.86
(76.92%)
📈 view plot
🚷 view threshold
749.20
(0.00%)Baseline: 749.20
524.44
(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
16.46
(0.00%)Baseline: 16.46
21.40
(76.92%)
📈 view plot
🚷 view threshold
24.37
(0.00%)Baseline: 24.37
31.68
(76.92%)
📈 view plot
🚷 view threshold
217.27
(0.00%)Baseline: 217.27
282.45
(76.92%)
📈 view plot
🚷 view threshold
476.21
(0.00%)Baseline: 476.21
333.35
(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
8.75
(0.00%)Baseline: 8.75
11.38
(76.92%)
📈 view plot
🚷 view threshold
16.99
(0.00%)Baseline: 16.99
22.09
(76.92%)
📈 view plot
🚷 view threshold
99.40
(0.00%)Baseline: 99.40
129.22
(76.92%)
📈 view plot
🚷 view threshold
772.83
(0.00%)Baseline: 772.83
540.98
(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
15.96
(0.00%)Baseline: 15.96
20.75
(76.92%)
📈 view plot
🚷 view threshold
23.99
(0.00%)Baseline: 23.99
31.19
(76.92%)
📈 view plot
🚷 view threshold
171.57
(0.00%)Baseline: 171.57
223.04
(76.92%)
📈 view plot
🚷 view threshold
482.40
(0.00%)Baseline: 482.40
337.68
(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
8.95
(0.00%)Baseline: 8.95
11.63
(76.92%)
📈 view plot
🚷 view threshold
16.43
(0.00%)Baseline: 16.43
21.36
(76.92%)
📈 view plot
🚷 view threshold
136.34
(0.00%)Baseline: 136.34
177.24
(76.92%)
📈 view plot
🚷 view threshold
747.62
(0.00%)Baseline: 747.62
523.33
(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,119.69
(0.00%)Baseline: 2,119.69
2,755.60
(76.92%)
📈 view plot
🚷 view threshold
2,146.17
(0.00%)Baseline: 2,146.17
2,790.02
(76.92%)
📈 view plot
🚷 view threshold
9,131.75
(0.00%)Baseline: 9,131.75
11,871.27
(76.92%)
📈 view plot
🚷 view threshold
3.98
(0.00%)Baseline: 3.98
2.79
(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
8.91
(0.00%)Baseline: 8.91
11.58
(76.92%)
📈 view plot
🚷 view threshold
16.69
(0.00%)Baseline: 16.69
21.70
(76.92%)
📈 view plot
🚷 view threshold
100.61
(0.00%)Baseline: 100.61
130.79
(76.92%)
📈 view plot
🚷 view threshold
767.77
(0.00%)Baseline: 767.77
537.44
(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
15.73
(0.00%)Baseline: 15.73
20.45
(76.92%)
📈 view plot
🚷 view threshold
24.18
(0.00%)Baseline: 24.18
31.43
(76.92%)
📈 view plot
🚷 view threshold
166.35
(0.00%)Baseline: 166.35
216.25
(76.92%)
📈 view plot
🚷 view threshold
490.40
(0.00%)Baseline: 490.40
343.28
(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
337.78
(0.00%)Baseline: 337.78
439.11
(76.92%)
📈 view plot
🚷 view threshold
500.30
(0.00%)Baseline: 500.30
650.39
(76.92%)
📈 view plot
🚷 view threshold
3,612.70
(0.00%)Baseline: 3,612.70
4,696.51
(76.92%)
📈 view plot
🚷 view threshold
17.84
(0.00%)Baseline: 17.84
12.49
(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.67
(0.00%)Baseline: 17.67
22.97
(76.92%)
📈 view plot
🚷 view threshold
27.45
(0.00%)Baseline: 27.45
35.69
(76.92%)
📈 view plot
🚷 view threshold
195.63
(0.00%)Baseline: 195.63
254.32
(76.92%)
📈 view plot
🚷 view threshold
430.39
(0.00%)Baseline: 430.39
301.27
(70.00%)
/empty: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
6.06
(0.00%)Baseline: 6.06
7.88
(76.92%)
📈 view plot
🚷 view threshold
8.06
(0.00%)Baseline: 8.06
10.48
(76.92%)
📈 view plot
🚷 view threshold
33.14
(0.00%)Baseline: 33.14
43.08
(76.92%)
📈 view plot
🚷 view threshold
1,452.73
(0.00%)Baseline: 1,452.73
1,016.91
(70.00%)
/image_example: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
346.88
(0.00%)Baseline: 346.88
450.94
(76.92%)
📈 view plot
🚷 view threshold
438.09
(0.00%)Baseline: 438.09
569.52
(76.92%)
📈 view plot
🚷 view threshold
3,955.24
(0.00%)Baseline: 3,955.24
5,141.81
(76.92%)
📈 view plot
🚷 view threshold
23.16
(0.00%)Baseline: 23.16
16.21
(70.00%)
/image_example: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
20.88
(0.00%)Baseline: 20.88
27.14
(76.92%)
📈 view plot
🚷 view threshold
29.03
(0.00%)Baseline: 29.03
37.74
(76.92%)
📈 view plot
🚷 view threshold
116.85
(0.00%)Baseline: 116.85
151.91
(76.92%)
📈 view plot
🚷 view threshold
422.70
(0.00%)Baseline: 422.70
295.89
(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
47.50
(0.00%)Baseline: 47.50
61.75
(76.92%)
📈 view plot
🚷 view threshold
74.99
(0.00%)Baseline: 74.99
97.49
(76.92%)
📈 view plot
🚷 view threshold
469.90
(0.00%)Baseline: 469.90
610.87
(76.92%)
📈 view plot
🚷 view threshold
160.23
(0.00%)Baseline: 160.23
112.16
(70.00%)
/loadable: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
16.64
(0.00%)Baseline: 16.64
21.63
(76.92%)
📈 view plot
🚷 view threshold
23.28
(0.00%)Baseline: 23.28
30.26
(76.92%)
📈 view plot
🚷 view threshold
481.09
(0.00%)Baseline: 481.09
625.42
(76.92%)
📈 view plot
🚷 view threshold
276.91
(0.00%)Baseline: 276.91
193.84
(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
8.87
(0.00%)Baseline: 8.87
11.53
(76.92%)
📈 view plot
🚷 view threshold
16.40
(0.00%)Baseline: 16.40
21.32
(76.92%)
📈 view plot
🚷 view threshold
95.98
(0.00%)Baseline: 95.98
124.77
(76.92%)
📈 view plot
🚷 view threshold
784.33
(0.00%)Baseline: 784.33
549.03
(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
84.42
(0.00%)Baseline: 84.42
109.75
(76.92%)
📈 view plot
🚷 view threshold
976.75
(0.00%)Baseline: 976.75
683.73
(70.00%)
/pure_component: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
293.66
(0.00%)Baseline: 293.66
381.76
(76.92%)
📈 view plot
🚷 view threshold
381.83
(0.00%)Baseline: 381.83
496.38
(76.92%)
📈 view plot
🚷 view threshold
7,278.46
(0.00%)Baseline: 7,278.46
9,462.00
(76.92%)
📈 view plot
🚷 view threshold
17.16
(0.00%)Baseline: 17.16
12.01
(70.00%)
/pure_component: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
17.30
(0.00%)Baseline: 17.30
22.49
(76.92%)
📈 view plot
🚷 view threshold
26.81
(0.00%)Baseline: 26.81
34.85
(76.92%)
📈 view plot
🚷 view threshold
184.17
(0.00%)Baseline: 184.17
239.42
(76.92%)
📈 view plot
🚷 view threshold
440.28
(0.00%)Baseline: 440.28
308.20
(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.66
(0.00%)Baseline: 10.66
13.86
(76.92%)
📈 view plot
🚷 view threshold
18.12
(0.00%)Baseline: 18.12
23.56
(76.92%)
📈 view plot
🚷 view threshold
65.15
(0.00%)Baseline: 65.15
84.70
(76.92%)
📈 view plot
🚷 view threshold
762.16
(0.00%)Baseline: 762.16
533.51
(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
18.13
(0.00%)Baseline: 18.13
23.57
(76.92%)
📈 view plot
🚷 view threshold
25.35
(0.00%)Baseline: 25.35
32.95
(76.92%)
📈 view plot
🚷 view threshold
105.06
(0.00%)Baseline: 105.06
136.58
(76.92%)
📈 view plot
🚷 view threshold
486.55
(0.00%)Baseline: 486.55
340.59
(70.00%)
/react_helmet: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
357.53
(0.00%)Baseline: 357.53
464.79
(76.92%)
📈 view plot
🚷 view threshold
443.86
(0.00%)Baseline: 443.86
577.02
(76.92%)
📈 view plot
🚷 view threshold
4,184.73
(0.00%)Baseline: 4,184.73
5,440.15
(76.92%)
📈 view plot
🚷 view threshold
22.99
(0.00%)Baseline: 22.99
16.09
(70.00%)
/react_helmet: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
21.32
(0.00%)Baseline: 21.32
27.72
(76.92%)
📈 view plot
🚷 view threshold
29.96
(0.00%)Baseline: 29.96
38.95
(76.92%)
📈 view plot
🚷 view threshold
121.29
(0.00%)Baseline: 121.29
157.68
(76.92%)
📈 view plot
🚷 view threshold
413.44
(0.00%)Baseline: 413.44
289.41
(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
357.53
(0.00%)Baseline: 357.53
464.79
(76.92%)
📈 view plot
🚷 view threshold
448.70
(0.00%)Baseline: 448.70
583.31
(76.92%)
📈 view plot
🚷 view threshold
4,080.08
(0.00%)Baseline: 4,080.08
5,304.10
(76.92%)
📈 view plot
🚷 view threshold
22.66
(0.00%)Baseline: 22.66
15.86
(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
14.37
(0.00%)Baseline: 14.37
18.68
(76.92%)
📈 view plot
🚷 view threshold
23.51
(0.00%)Baseline: 23.51
30.56
(76.92%)
📈 view plot
🚷 view threshold
227.94
(0.00%)Baseline: 227.94
296.32
(76.92%)
📈 view plot
🚷 view threshold
446.08
(0.00%)Baseline: 446.08
312.26
(70.00%)
/react_router: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
371.24
(0.00%)Baseline: 371.24
482.61
(76.92%)
📈 view plot
🚷 view threshold
462.09
(0.00%)Baseline: 462.09
600.72
(76.92%)
📈 view plot
🚷 view threshold
4,152.26
(0.00%)Baseline: 4,152.26
5,397.94
(76.92%)
📈 view plot
🚷 view threshold
22.32
(0.00%)Baseline: 22.32
15.62
(70.00%)
/react_router: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
15.05
(0.00%)Baseline: 15.05
19.57
(76.92%)
📈 view plot
🚷 view threshold
26.56
(0.00%)Baseline: 26.56
34.53
(76.92%)
📈 view plot
🚷 view threshold
155.52
(0.00%)Baseline: 155.52
202.18
(76.92%)
📈 view plot
🚷 view threshold
478.70
(0.00%)Baseline: 478.70
335.09
(70.00%)
/redis_receiver: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
78.30
(0.00%)Baseline: 78.30
101.79
(76.92%)
📈 view plot
🚷 view threshold
124.27
(0.00%)Baseline: 124.27
161.55
(76.92%)
📈 view plot
🚷 view threshold
575.93
(0.00%)Baseline: 575.93
748.71
(76.92%)
📈 view plot
🚷 view threshold
106.36
(0.00%)Baseline: 106.36
74.45
(70.00%)
/render_js: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
327.81
(0.00%)Baseline: 327.81
426.15
(76.92%)
📈 view plot
🚷 view threshold
413.48
(0.00%)Baseline: 413.48
537.52
(76.92%)
📈 view plot
🚷 view threshold
3,535.62
(0.00%)Baseline: 3,535.62
4,596.31
(76.92%)
📈 view plot
🚷 view threshold
24.85
(0.00%)Baseline: 24.85
17.40
(70.00%)
/render_js: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
19.65
(0.00%)Baseline: 19.65
25.55
(76.92%)
📈 view plot
🚷 view threshold
27.90
(0.00%)Baseline: 27.90
36.27
(76.92%)
📈 view plot
🚷 view threshold
110.81
(0.00%)Baseline: 110.81
144.05
(76.92%)
📈 view plot
🚷 view threshold
447.96
(0.00%)Baseline: 447.96
313.57
(70.00%)
/rendered_html: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
339.57
(0.00%)Baseline: 339.57
441.44
(76.92%)
📈 view plot
🚷 view threshold
435.80
(0.00%)Baseline: 435.80
566.54
(76.92%)
📈 view plot
🚷 view threshold
3,924.02
(0.00%)Baseline: 3,924.02
5,101.23
(76.92%)
📈 view plot
🚷 view threshold
23.58
(0.00%)Baseline: 23.58
16.51
(70.00%)
/rendered_html: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
14.44
(0.00%)Baseline: 14.44
18.77
(76.92%)
📈 view plot
🚷 view threshold
23.63
(0.00%)Baseline: 23.63
30.72
(76.92%)
📈 view plot
🚷 view threshold
228.15
(0.00%)Baseline: 228.15
296.60
(76.92%)
📈 view plot
🚷 view threshold
442.46
(0.00%)Baseline: 442.46
309.72
(70.00%)
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 1, 2026

🐰 Bencher Report

Branchjg/issue-2332
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
18.68
(0.00%)Baseline: 18.68
24.28
(76.92%)
📈 view plot
🚷 view threshold
28.83
(0.00%)Baseline: 28.83
37.48
(76.92%)
📈 view plot
🚷 view threshold
206.95
(0.00%)Baseline: 206.95
269.03
(76.92%)
📈 view plot
🚷 view threshold
417.94
(0.00%)Baseline: 417.94
292.56
(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
65.90
(0.00%)Baseline: 65.90
85.67
(76.92%)
📈 view plot
🚷 view threshold
96.89
(0.00%)Baseline: 96.89
125.96
(76.92%)
📈 view plot
🚷 view threshold
668.67
(0.00%)Baseline: 668.67
869.27
(76.92%)
📈 view plot
🚷 view threshold
115.50
(0.00%)Baseline: 115.50
80.85
(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
20.49
(0.00%)Baseline: 20.49
26.64
(76.92%)
📈 view plot
🚷 view threshold
28.67
(0.00%)Baseline: 28.67
37.27
(76.92%)
📈 view plot
🚷 view threshold
116.45
(0.00%)Baseline: 116.45
151.38
(76.92%)
📈 view plot
🚷 view threshold
433.19
(0.00%)Baseline: 433.19
303.23
(70.00%)
/server_router: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
18.87
(0.00%)Baseline: 18.87
24.53
(76.92%)
📈 view plot
🚷 view threshold
29.58
(0.00%)Baseline: 29.58
38.45
(76.92%)
📈 view plot
🚷 view threshold
200.95
(0.00%)Baseline: 200.95
261.24
(76.92%)
📈 view plot
🚷 view threshold
408.26
(0.00%)Baseline: 408.26
285.78
(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
17.23
(0.00%)Baseline: 17.23
22.40
(76.92%)
📈 view plot
🚷 view threshold
25.24
(0.00%)Baseline: 25.24
32.81
(76.92%)
📈 view plot
🚷 view threshold
230.16
(0.00%)Baseline: 230.16
299.21
(76.92%)
📈 view plot
🚷 view threshold
456.96
(0.00%)Baseline: 456.96
319.87
(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
345.72
(0.00%)Baseline: 345.72
449.44
(76.92%)
📈 view plot
🚷 view threshold
433.02
(0.00%)Baseline: 433.02
562.93
(76.92%)
📈 view plot
🚷 view threshold
3,893.34
(0.00%)Baseline: 3,893.34
5,061.34
(76.92%)
📈 view plot
🚷 view threshold
23.56
(0.00%)Baseline: 23.56
16.49
(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.02
(0.00%)Baseline: 18.02
23.43
(76.92%)
📈 view plot
🚷 view threshold
27.78
(0.00%)Baseline: 27.78
36.11
(76.92%)
📈 view plot
🚷 view threshold
189.13
(0.00%)Baseline: 189.13
245.87
(76.92%)
📈 view plot
🚷 view threshold
423.61
(0.00%)Baseline: 423.61
296.53
(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
348.54
(0.00%)Baseline: 348.54
453.10
(76.92%)
📈 view plot
🚷 view threshold
432.62
(0.00%)Baseline: 432.62
562.41
(76.92%)
📈 view plot
🚷 view threshold
3,939.16
(0.00%)Baseline: 3,939.16
5,120.91
(76.92%)
📈 view plot
🚷 view threshold
23.46
(0.00%)Baseline: 23.46
16.42
(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
20.74
(0.00%)Baseline: 20.74
26.96
(76.92%)
📈 view plot
🚷 view threshold
28.90
(0.00%)Baseline: 28.90
37.57
(76.92%)
📈 view plot
🚷 view threshold
120.10
(0.00%)Baseline: 120.10
156.13
(76.92%)
📈 view plot
🚷 view threshold
427.50
(0.00%)Baseline: 427.50
299.25
(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
385.60
(0.00%)Baseline: 385.60
501.28
(76.92%)
📈 view plot
🚷 view threshold
434.01
(0.00%)Baseline: 434.01
564.21
(76.92%)
📈 view plot
🚷 view threshold
2,553.86
(0.00%)Baseline: 2,553.86
3,320.02
(76.92%)
📈 view plot
🚷 view threshold
23.48
(0.00%)Baseline: 23.48
16.44
(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
20.21
(0.00%)Baseline: 20.21
26.27
(76.92%)
📈 view plot
🚷 view threshold
28.11
(0.00%)Baseline: 28.11
36.54
(76.92%)
📈 view plot
🚷 view threshold
114.89
(0.00%)Baseline: 114.89
149.36
(76.92%)
📈 view plot
🚷 view threshold
440.32
(0.00%)Baseline: 440.32
308.22
(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
350.51
(0.00%)Baseline: 350.51
455.66
(76.92%)
📈 view plot
🚷 view threshold
434.23
(0.00%)Baseline: 434.23
564.50
(76.92%)
📈 view plot
🚷 view threshold
4,005.74
(0.00%)Baseline: 4,005.74
5,207.46
(76.92%)
📈 view plot
🚷 view threshold
23.37
(0.00%)Baseline: 23.37
16.36
(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
702.96
(0.00%)Baseline: 702.96
913.85
(76.92%)
📈 view plot
🚷 view threshold
951.32
(0.00%)Baseline: 951.32
1,236.72
(76.92%)
📈 view plot
🚷 view threshold
8,871.77
(0.00%)Baseline: 8,871.77
11,533.30
(76.92%)
📈 view plot
🚷 view threshold
11.32
(0.00%)Baseline: 11.32
7.92
(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
21.28
(0.00%)Baseline: 21.28
27.66
(76.92%)
📈 view plot
🚷 view threshold
31.00
(0.00%)Baseline: 31.00
40.30
(76.92%)
📈 view plot
🚷 view threshold
225.03
(0.00%)Baseline: 225.03
292.54
(76.92%)
📈 view plot
🚷 view threshold
372.05
(0.00%)Baseline: 372.05
260.44
(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
719.31
(0.00%)Baseline: 719.31
935.10
(76.92%)
📈 view plot
🚷 view threshold
961.29
(0.00%)Baseline: 961.29
1,249.68
(76.92%)
📈 view plot
🚷 view threshold
7,269.30
(0.00%)Baseline: 7,269.30
9,450.09
(76.92%)
📈 view plot
🚷 view threshold
11.49
(0.00%)Baseline: 11.49
8.04
(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
24.22
(0.00%)Baseline: 24.22
31.49
(76.92%)
📈 view plot
🚷 view threshold
32.75
(0.00%)Baseline: 32.75
42.58
(76.92%)
📈 view plot
🚷 view threshold
134.46
(0.00%)Baseline: 134.46
174.80
(76.92%)
📈 view plot
🚷 view threshold
368.31
(0.00%)Baseline: 368.31
257.82
(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
717.04
(0.00%)Baseline: 717.04
932.15
(76.92%)
📈 view plot
🚷 view threshold
966.09
(0.00%)Baseline: 966.09
1,255.92
(76.92%)
📈 view plot
🚷 view threshold
7,465.99
(0.00%)Baseline: 7,465.99
9,705.79
(76.92%)
📈 view plot
🚷 view threshold
11.46
(0.00%)Baseline: 11.46
8.02
(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.64
(0.00%)Baseline: 21.64
28.13
(76.92%)
📈 view plot
🚷 view threshold
34.06
(0.00%)Baseline: 34.06
44.28
(76.92%)
📈 view plot
🚷 view threshold
250.93
(0.00%)Baseline: 250.93
326.21
(76.92%)
📈 view plot
🚷 view threshold
366.52
(0.00%)Baseline: 366.52
256.56
(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
388.61
(0.00%)Baseline: 388.61
505.19
(76.92%)
📈 view plot
🚷 view threshold
436.18
(0.00%)Baseline: 436.18
567.03
(76.92%)
📈 view plot
🚷 view threshold
3,097.23
(0.00%)Baseline: 3,097.23
4,026.40
(76.92%)
📈 view plot
🚷 view threshold
23.50
(0.00%)Baseline: 23.50
16.45
(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
13.44
(0.00%)Baseline: 13.44
17.47
(76.92%)
📈 view plot
🚷 view threshold
25.33
(0.00%)Baseline: 25.33
32.93
(76.92%)
📈 view plot
🚷 view threshold
347.40
(0.00%)Baseline: 347.40
451.62
(76.92%)
📈 view plot
🚷 view threshold
421.05
(0.00%)Baseline: 421.05
294.74
(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
363.98
(0.00%)Baseline: 363.98
473.17
(76.92%)
📈 view plot
🚷 view threshold
448.22
(0.00%)Baseline: 448.22
582.69
(76.92%)
📈 view plot
🚷 view threshold
4,061.22
(0.00%)Baseline: 4,061.22
5,279.59
(76.92%)
📈 view plot
🚷 view threshold
22.81
(0.00%)Baseline: 22.81
15.97
(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
20.70
(0.00%)Baseline: 20.70
26.91
(76.92%)
📈 view plot
🚷 view threshold
28.61
(0.00%)Baseline: 28.61
37.19
(76.92%)
📈 view plot
🚷 view threshold
121.32
(0.00%)Baseline: 121.32
157.72
(76.92%)
📈 view plot
🚷 view threshold
430.22
(0.00%)Baseline: 430.22
301.15
(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
396.01
(0.00%)Baseline: 396.01
514.81
(76.92%)
📈 view plot
🚷 view threshold
447.65
(0.00%)Baseline: 447.65
581.94
(76.92%)
📈 view plot
🚷 view threshold
2,679.95
(0.00%)Baseline: 2,679.95
3,483.93
(76.92%)
📈 view plot
🚷 view threshold
23.10
(0.00%)Baseline: 23.10
16.17
(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
17.26
(0.00%)Baseline: 17.26
22.44
(76.92%)
📈 view plot
🚷 view threshold
27.02
(0.00%)Baseline: 27.02
35.13
(76.92%)
📈 view plot
🚷 view threshold
196.51
(0.00%)Baseline: 196.51
255.46
(76.92%)
📈 view plot
🚷 view threshold
458.72
(0.00%)Baseline: 458.72
321.10
(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
357.15
(0.00%)Baseline: 357.15
464.30
(76.92%)
📈 view plot
🚷 view threshold
444.83
(0.00%)Baseline: 444.83
578.28
(76.92%)
📈 view plot
🚷 view threshold
4,156.39
(0.00%)Baseline: 4,156.39
5,403.31
(76.92%)
📈 view plot
🚷 view threshold
23.08
(0.00%)Baseline: 23.08
16.16
(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.70
(0.00%)Baseline: 9.70
12.61
(76.92%)
📈 view plot
🚷 view threshold
14.34
(0.00%)Baseline: 14.34
18.64
(76.92%)
📈 view plot
🚷 view threshold
102.78
(0.00%)Baseline: 102.78
133.61
(76.92%)
📈 view plot
🚷 view threshold
804.87
(0.00%)Baseline: 804.87
563.41
(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
10.11
(0.00%)Baseline: 10.11
13.14
(76.92%)
📈 view plot
🚷 view threshold
16.49
(0.00%)Baseline: 16.49
21.44
(76.92%)
📈 view plot
🚷 view threshold
110.57
(0.00%)Baseline: 110.57
143.74
(76.92%)
📈 view plot
🚷 view threshold
575.52
(0.00%)Baseline: 575.52
402.86
(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
15.37
(0.00%)Baseline: 15.37
19.98
(76.92%)
📈 view plot
🚷 view threshold
23.97
(0.00%)Baseline: 23.97
31.16
(76.92%)
📈 view plot
🚷 view threshold
170.73
(0.00%)Baseline: 170.73
221.95
(76.92%)
📈 view plot
🚷 view threshold
517.14
(0.00%)Baseline: 517.14
362.00
(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
349.88
(0.00%)Baseline: 349.88
454.84
(76.92%)
📈 view plot
🚷 view threshold
526.67
(0.00%)Baseline: 526.67
684.67
(76.92%)
📈 view plot
🚷 view threshold
3,806.23
(0.00%)Baseline: 3,806.23
4,948.10
(76.92%)
📈 view plot
🚷 view threshold
17.14
(0.00%)Baseline: 17.14
12.00
(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
18.78
(0.00%)Baseline: 18.78
24.41
(76.92%)
📈 view plot
🚷 view threshold
29.66
(0.00%)Baseline: 29.66
38.56
(76.92%)
📈 view plot
🚷 view threshold
212.33
(0.00%)Baseline: 212.33
276.03
(76.92%)
📈 view plot
🚷 view threshold
419.73
(0.00%)Baseline: 419.73
293.81
(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.63
(0.00%)Baseline: 9.63
12.52
(76.92%)
📈 view plot
🚷 view threshold
17.22
(0.00%)Baseline: 17.22
22.39
(76.92%)
📈 view plot
🚷 view threshold
142.65
(0.00%)Baseline: 142.65
185.44
(76.92%)
📈 view plot
🚷 view threshold
715.64
(0.00%)Baseline: 715.64
500.95
(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.76
(0.00%)Baseline: 19.76
25.69
(76.92%)
📈 view plot
🚷 view threshold
27.44
(0.00%)Baseline: 27.44
35.67
(76.92%)
📈 view plot
🚷 view threshold
112.62
(0.00%)Baseline: 112.62
146.41
(76.92%)
📈 view plot
🚷 view threshold
448.53
(0.00%)Baseline: 448.53
313.97
(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
13.43
(0.00%)Baseline: 13.43
17.46
(76.92%)
📈 view plot
🚷 view threshold
25.58
(0.00%)Baseline: 25.58
33.25
(76.92%)
📈 view plot
🚷 view threshold
338.46
(0.00%)Baseline: 338.46
440.00
(76.92%)
📈 view plot
🚷 view threshold
416.01
(0.00%)Baseline: 416.01
291.21
(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
10.36
(0.00%)Baseline: 10.36
13.47
(76.92%)
📈 view plot
🚷 view threshold
17.14
(0.00%)Baseline: 17.14
22.28
(76.92%)
📈 view plot
🚷 view threshold
63.74
(0.00%)Baseline: 63.74
82.86
(76.92%)
📈 view plot
🚷 view threshold
796.20
(0.00%)Baseline: 796.20
557.34
(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
8.97
(0.00%)Baseline: 8.97
11.66
(76.92%)
📈 view plot
🚷 view threshold
17.00
(0.00%)Baseline: 17.00
22.10
(76.92%)
📈 view plot
🚷 view threshold
96.91
(0.00%)Baseline: 96.91
125.98
(76.92%)
📈 view plot
🚷 view threshold
767.63
(0.00%)Baseline: 767.63
537.34
(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
11.98
(0.00%)Baseline: 11.98
15.57
(76.92%)
📈 view plot
🚷 view threshold
21.99
(0.00%)Baseline: 21.99
28.59
(76.92%)
📈 view plot
🚷 view threshold
311.93
(0.00%)Baseline: 311.93
405.51
(76.92%)
📈 view plot
🚷 view threshold
472.82
(0.00%)Baseline: 472.82
330.97
(70.00%)
/xhr_refresh: Core📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
671.65
(0.00%)Baseline: 671.65
873.14
(76.92%)
📈 view plot
🚷 view threshold
978.47
(0.00%)Baseline: 978.47
1,272.01
(76.92%)
📈 view plot
🚷 view threshold
6,798.09
(0.00%)Baseline: 6,798.09
8,837.52
(76.92%)
📈 view plot
🚷 view threshold
11.93
(0.00%)Baseline: 11.93
8.35
(70.00%)
/xhr_refresh: Pro📈 view plot
🚷 view threshold
0.00
(0.00%)Baseline: 0.00
0.00
(0.00%)
📈 view plot
🚷 view threshold
20.74
(0.00%)Baseline: 20.74
26.96
(76.92%)
📈 view plot
🚷 view threshold
31.00
(0.00%)Baseline: 31.00
40.30
(76.92%)
📈 view plot
🚷 view threshold
219.60
(0.00%)Baseline: 219.60
285.48
(76.92%)
📈 view plot
🚷 view threshold
374.80
(0.00%)Baseline: 374.80
262.36
(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.70
(0.00%)Baseline: 4.70
6.11
(76.92%)
📈 view plot
🚷 view threshold
5.69
(0.00%)Baseline: 5.69
7.40
(76.92%)
📈 view plot
🚷 view threshold
9.14
(0.00%)Baseline: 9.14
11.88
(76.92%)
📈 view plot
🚷 view threshold
1,956.47
(0.00%)Baseline: 1,956.47
1,369.53
(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.20
(0.00%)Baseline: 4.20
5.46
(76.92%)
📈 view plot
🚷 view threshold
5.23
(0.00%)Baseline: 5.23
6.80
(76.92%)
📈 view plot
🚷 view threshold
8.05
(0.00%)Baseline: 8.05
10.46
(76.92%)
📈 view plot
🚷 view threshold
2,163.24
(0.00%)Baseline: 2,163.24
1,514.27
(70.00%)
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 1, 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.48 KB (0%)
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%)

justin808 and others added 6 commits February 3, 2026 15:24
Add support for various license plan types beyond just "paid":
- startup: Complimentary for qualifying startups
- nonprofit: Complimentary for non-profits
- education: For educational institutions
- oss: For open source projects
- partner: Strategic partners

Changes:
- Add VALID_PLANS constant with all supported plan types
- Add license_plan method to retrieve cached plan type
- Update check_plan to validate against VALID_PLANS
- Update logging to show plan type for non-paid licenses
- Add comprehensive tests for all plan types

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Extract shared JWT decoding logic into with_decoded_license helper
- Add clarifying comment for plan type logging logic

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Document that determine_license_plan returns nil for invalid plans
- Add comment explaining dynamic test generation from VALID_PLANS
- Document behavior when unknown plan types are used

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add changelog entry for the multiple license plan types feature
that supports: paid, startup, nonprofit, education, oss, and partner.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Update tests to include org field in license payloads and match the
new message format that includes organization name.

Changes:
- Add org field to startup, nonprofit, and oss license payloads
- Update expected messages to include organization name in format
  "License validated successfully (OrgName - plan license)."
- Update paid license test to expect "(Acme Corp)" in message

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add the missing getLicensePlan() function to the TypeScript license
validator to match the Ruby implementation. This provides:
- getLicensePlan() returning ValidPlan type or undefined
- Caching for performance (like getLicenseStatus/getLicenseOrganization)
- Export of ValidPlan type for consumers
- Updated reset() to clear plan cache
- Comprehensive test coverage for all plan types

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

@greptile review this pr

@justin808
Copy link
Copy Markdown
Member Author

@claude review this PR

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 4, 2026

PR Review: Support multiple license plan types (#2334)

Overview

This PR extends the license validator to support multiple plan types beyond "paid" (startup, nonprofit, education, oss, partner). The implementation looks solid overall with good test coverage and consistent changes across both Ruby and TypeScript implementations.

✅ Strengths

1. Excellent Test Coverage

  • Added comprehensive tests for all 6 plan types in both Ruby and TypeScript
  • Tests cover edge cases: missing plans, invalid plans, invalid signatures
  • Tests verify caching behavior for the new license_plan method
  • Dynamic test generation using VALID_PLANS.each ensures tests stay in sync with code

2. Good Code Consistency

  • VALID_PLANS constants kept in sync between Ruby and TypeScript with comments noting the requirement
  • Both implementations follow the same validation logic
  • Caching implemented consistently in both languages

3. Backwards Compatibility

  • Paid licenses without plan type continue to work (backwards compatible with old licenses)
  • Log messages for paid licenses remain unchanged (no "paid license" suffix)
  • Thread-safe caching maintained in Ruby implementation

4. Clear Documentation

  • Good inline comments explaining plan types and their purposes
  • Updated changelog entry clearly describes the feature
  • Test descriptions are clear and specific

🔍 Potential Issues & Suggestions

1. Code Duplication in Tests (Minor)

Ruby tests (engine_spec.rb): The test structure for different plan types is very similar. Consider DRY-ing it up using an .each loop similar to what's done in license_validator_spec.rb. Low priority - tests work fine as-is.

2. Missing Edge Case Tests

Consider adding tests for:

  • Empty string plan: { plan: "" }
  • Null plan: { plan: null }

These are low priority edge cases but would make coverage more complete.

3. Thread Safety Documentation (TypeScript)

The Ruby implementation has excellent documentation about thread safety and the LICENSE_MUTEX. The TypeScript implementation uses module-level caching but doesn't document whether it's safe in Node.js cluster/worker scenarios.

Suggestion: Add a comment about whether the caching is safe with the Node renderer's worker model.

4. Excellent Refactoring: with_decoded_license Helper

The introduction of the with_decoded_license helper method is a great refactoring that:

  • Eliminates code duplication
  • Makes the code more maintainable
  • Provides a consistent pattern for license decoding

This is well done! 👍

🔒 Security Considerations

No Security Issues Found

  • JWT validation still enforces RS256 algorithm only (prevents "alg=none" attacks)
  • Public key verification maintained
  • No SQL injection vectors
  • No XSS vectors (license data only logged server-side)
  • Plan validation happens before any trust is placed in the value
  • Backwards compatibility doesn't weaken security

🚀 Performance Considerations

Good Performance Characteristics

  • Caching prevents repeated JWT decoding
  • Thread-safe initialization in Ruby (LICENSE_MUTEX)
  • No N+1 queries or expensive operations
  • Early returns optimize the happy path
  • The with_decoded_license helper improves performance by centralizing decode logic

📋 Test Coverage Assessment

Comprehensive Coverage

Ruby tests (license_validator_spec.rb):

  • ✅ All 6 valid plan types tested
  • ✅ Invalid plan type ("free") tested
  • ✅ Missing plan field tested
  • ✅ Invalid signature tested
  • ✅ Caching tested
  • ✅ Reset behavior tested

TypeScript tests (licenseValidator.test.ts):

  • ✅ All 6 valid plan types tested (using it.each)
  • ✅ Invalid plan type tested
  • ✅ Missing plan field tested
  • ✅ Missing license tested
  • ✅ Invalid signature tested
  • ✅ Caching tested
  • ✅ Reset behavior tested

Integration tests (engine_spec.rb):

  • ✅ Paid license logging (without plan suffix)
  • ✅ Startup, nonprofit, oss license logging (with plan suffix)
  • ✅ Tests in both production and non-production contexts

🎯 Recommendations

Must Fix (None)

No blocking issues found.

Should Fix (Optional - for follow-up)

  1. Consider DRY-ing up the engine tests to reduce duplication
  2. Add edge case tests for empty/null plan values
  3. Add comment about TypeScript caching thread/worker safety

✅ Final Verdict

APPROVE

This is a well-implemented feature with excellent test coverage and good attention to detail. The code is clean, follows project conventions, maintains backwards compatibility, and has no security issues. The minor suggestions above are optimizations and edge case handling that don't block the PR.

The implementation correctly:

  • Extends license validation without breaking existing licenses
  • Maintains thread safety in Ruby
  • Provides proper caching
  • Has comprehensive tests
  • Documents changes clearly
  • Keeps Ruby and TypeScript implementations in sync
  • Includes a nice refactoring with the with_decoded_license helper

Great work! 🎉

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 4, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 4, 2026

Code Review for PR #2334: Support multiple license plan types

This PR successfully implements support for multiple license plan types. Here's my detailed review:

✅ Strengths

1. Excellent Implementation Consistency

  • Both Ruby and TypeScript implementations are perfectly synchronized with identical logic
  • VALID_PLANS constant is maintained in sync across both files with clear documentation
  • Thread safety considerations are well-documented in both languages (Mutex in Ruby, module-level state explanation in Node.js)

2. Comprehensive Test Coverage

  • 164 new lines of TypeScript tests covering all plan types, edge cases, caching, and reset behavior
  • 48 new lines of Ruby specs with similar coverage
  • Tests use .each pattern to dynamically test all valid plan types - excellent for maintainability
  • Edge cases well covered: empty strings, null values, invalid signatures, missing licenses

3. Backwards Compatibility

  • Licenses without a plan field remain valid (line 122-123 in TS, line 278 in Ruby)
  • Empty string and null plan values are treated as absent - smart defensive programming
  • Paid licenses don't show plan type in logs, maintaining existing behavior

4. Security Best Practices

  • Signature verification remains strict (verify: true, algorithm: RS256)
  • Invalid plan types correctly return :invalid status
  • Organization field validation is robust (checks for string type AND non-empty after trim)

5. Documentation Quality

  • Thread safety explanation in TypeScript is exceptionally detailed (lines 40-59)
  • JSDoc and Ruby comments are clear and comprehensive
  • Changelog entry is well-formatted and descriptive

🔍 Minor Observations

1. Type Safety in TypeScript (line 125, 280, 462)

if (VALID_PLANS.includes(plan as ValidPlan)) {

The as ValidPlan cast is necessary because plan is string, but the pattern is correct. This is an unavoidable TypeScript limitation when checking runtime string values against a const array.

2. Empty String Plan Handling (TS line 234-248)

The test explicitly checks that empty string plans are treated as valid (backwards compat). Consider if this should be documented in the interface definition at line 20-21:

// Optional: license plan. See VALID_PLANS for accepted values.
// Empty/absent plan defaults to valid (backwards compatibility with old licenses)
plan?: string;

3. Caching Strategy

The caching is well-implemented with proper reset in tests. The cachedLicensePlan follows the same pattern as other cached values - excellent consistency.

🎯 Code Quality Metrics

  • DRY Principle: Logic is shared appropriately, no unnecessary duplication
  • SOLID Principles: Single responsibility maintained, functions are focused
  • Test Coverage: Comprehensive with edge cases
  • Performance: Caching prevents redundant JWT decoding
  • Security: No vulnerabilities introduced, validation remains strict
  • Maintainability: Dynamic test generation will catch issues when new plans are added

🚀 Performance Considerations

Positive impacts:

  • Caching prevents redundant JWT verification (CPU-intensive RSA operations)
  • Thread-safe lazy initialization minimizes overhead
  • In Node.js worker processes, each worker validates once then caches for its lifetime

No concerns: The implementation is efficient and follows best practices.

📋 Suggestions (Optional Enhancements)

1. Consider Adding Plan Validation Helper

For future use in other parts of the codebase:

# In LicenseValidator
def self.valid_plan?(plan)
  VALID_PLANS.include?(plan)
end

2. Future: Plan-Specific Behavior

The architecture now supports plan-specific features (attribution requirements, feature flags, etc.). The ATTRIBUTION_REQUIRED_PLANS constant at line 34 suggests this is already planned. 👍

✅ Final Verdict

Approved with high confidence. This PR is production-ready:

  • ✅ No security concerns
  • ✅ No performance regressions
  • ✅ Excellent test coverage
  • ✅ Perfect backwards compatibility
  • ✅ Well-documented and maintainable
  • ✅ Follows repository conventions (CLAUDE.md compliance)

The implementation quality is excellent, with careful attention to thread safety, caching, backwards compatibility, and test coverage. The synchronization between Ruby and TypeScript implementations is particularly impressive.

🎉 Great work, @justin808!

The dynamic test generation, comprehensive edge case coverage, and detailed thread safety documentation demonstrate thoughtful engineering. This establishes a solid foundation for supporting complimentary licenses.

@justin808 justin808 merged commit 93e6d40 into master Feb 4, 2026
22 of 23 checks passed
@justin808 justin808 deleted the jg/issue-2332 branch February 4, 2026 01:59
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.

Support multiple license plan types (startup, nonprofit, education, etc.)

1 participant