react_on_rails 17.0.1 → 17.1.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/docs/agent/README.md +26 -0
  4. data/docs/agent/doctor-fix-loop.md +25 -0
  5. data/docs/agent/install-and-upgrade.md +162 -0
  6. data/docs/agent/rsc-adoption.md +31 -0
  7. data/docs/agent/streaming-debug.md +57 -0
  8. data/lib/generators/react_on_rails/base_generator.rb +4 -4
  9. data/lib/generators/react_on_rails/generator_helper.rb +12 -1
  10. data/lib/generators/react_on_rails/install_generator.rb +50 -5
  11. data/lib/generators/react_on_rails/js_dependency_manager.rb +1 -1
  12. data/lib/generators/react_on_rails/pro_generator.rb +5 -3
  13. data/lib/generators/react_on_rails/pro_setup.rb +51 -19
  14. data/lib/generators/react_on_rails/rsc_generator.rb +18 -1
  15. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +63 -25
  16. data/lib/generators/react_on_rails/rsc_setup.rb +141 -39
  17. data/lib/generators/react_on_rails/templates/agent_files/AGENTS.md +31 -3
  18. data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev-static-assets → Procfile.dev-static-assets.tt} +1 -1
  19. data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev → Procfile.dev.tt} +1 -1
  20. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +18 -42
  21. data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb +215 -0
  22. data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_skill.md +71 -0
  23. data/lib/react_on_rails/agent_guardrails.rb +223 -0
  24. data/lib/react_on_rails/dev/service_checker.rb +1 -1
  25. data/lib/react_on_rails/doctor.rb +880 -48
  26. data/lib/react_on_rails/doctor_schema.rb +192 -0
  27. data/lib/react_on_rails/engine.rb +2 -2
  28. data/lib/react_on_rails/helper.rb +17 -10
  29. data/lib/react_on_rails/length_prefixed_parser.rb +22 -14
  30. data/lib/react_on_rails/lenient_json.rb +0 -0
  31. data/lib/react_on_rails/prerender_error.rb +38 -8
  32. data/lib/react_on_rails/react_component/render_options.rb +1 -1
  33. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +168 -15
  34. data/lib/react_on_rails/version.rb +1 -1
  35. data/lib/tasks/agent_guardrails.rake +21 -0
  36. data/rakelib/run_rspec.rake +14 -0
  37. data/sig/react_on_rails/prerender_error.rbs +2 -1
  38. data/skills/doctor-fix-loop/SKILL.md +26 -0
  39. data/skills/install-and-upgrade/SKILL.md +24 -0
  40. data/skills/rsc-adoption/SKILL.md +28 -0
  41. data/skills/streaming-debug/SKILL.md +53 -0
  42. metadata +18 -4
  43. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch +0 -26
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRails
4
+ # Public contract for the machine-readable doctor report.
5
+ #
6
+ # Check IDs are stable identifiers for automation. Never rename or reuse one;
7
+ # add a new ID for a new check. Additive fields may be introduced without a
8
+ # schema version bump, but removing or changing a documented field requires a
9
+ # new version.
10
+ module DoctorSchema
11
+ VERSION = 1
12
+ STATUS_SEVERITIES = { "pass" => "info", "warn" => "warning", "fail" => "error" }.freeze
13
+ STATUSES = STATUS_SEVERITIES.keys.freeze
14
+ SEVERITIES = STATUS_SEVERITIES.values.uniq.freeze
15
+ DETAIL_LEVELS = %w[success warning error info].freeze
16
+ EXIT_CODES = { pass: 0, warn: 0, fail: 1 }.freeze
17
+ DOCS_URL = "https://reactonrails.com/docs/api-reference/doctor"
18
+
19
+ CHECK_METADATA = {
20
+ "environment_prerequisites" => {
21
+ files: ["package.json", "package manager lockfile"],
22
+ expected_end_state: "Node.js and the app's JavaScript package manager are installed and detected."
23
+ },
24
+ "react_on_rails_versions" => {
25
+ files: ["Gemfile", "package.json", "package manager lockfile"],
26
+ expected_end_state: "The React on Rails gem and npm package versions are compatible and pinned safely."
27
+ },
28
+ "react_on_rails_packages" => {
29
+ files: ["Gemfile", "config/shakapacker.yml", "config/webpack", "config/rspack"],
30
+ expected_end_state: "Shakapacker is installed and its configured bundler files exist."
31
+ },
32
+ "javascript_package_dependencies" => {
33
+ files: ["package.json", "package manager lockfile"],
34
+ expected_end_state: "Required React and React on Rails JavaScript dependencies are installed and compatible."
35
+ },
36
+ "key_configuration_files" => {
37
+ files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/javascript"],
38
+ expected_end_state: "The generated React on Rails configuration files exist and match the app setup."
39
+ },
40
+ "configuration_analysis" => {
41
+ files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/views/layouts"],
42
+ expected_end_state: "React on Rails, Shakapacker, and server bundle settings agree."
43
+ },
44
+ "bin_dev_launcher_setup" => {
45
+ files: ["bin/dev", "Procfile.dev", "Procfile.dev-static-assets"],
46
+ expected_end_state: "The development launcher starts Rails and the configured asset bundler correctly."
47
+ },
48
+ "rails_integration" => {
49
+ files: ["config/initializers/react_on_rails.rb", "config/application.rb"],
50
+ expected_end_state: "Rails loads the React on Rails initializer successfully."
51
+ },
52
+ "bundler_configuration" => {
53
+ files: ["config/shakapacker.yml", "config/webpack", "config/rspack"],
54
+ expected_end_state: "The configured webpack or Rspack entry point is present and valid."
55
+ },
56
+ "testing_setup" => {
57
+ files: ["spec/rails_helper.rb", "spec/spec_helper.rb", "test/test_helper.rb"],
58
+ expected_end_state: "The test suite builds React assets before tests that need them."
59
+ },
60
+ "development_environment" => {
61
+ files: ["config/environments/development.rb", "config/shakapacker.yml", "package.json"],
62
+ expected_end_state: "The development server, HMR, and generated bundle settings are consistent."
63
+ },
64
+ "react_on_rails_pro_setup" => {
65
+ files: ["Gemfile", "package.json", "config/initializers/react_on_rails.rb"],
66
+ expected_end_state: "React on Rails Pro packages and configuration are mutually compatible."
67
+ },
68
+ "node_renderer_rollout_capacity" => {
69
+ files: ["renderer/node-renderer.js", "client/node-renderer.js", "deployment environment"],
70
+ expected_end_state:
71
+ "The NodeRenderer declares and prewarms its current server/RSC set, and its VM hard cap can hold " \
72
+ "rollout overlap."
73
+ },
74
+ "react_server_components" => {
75
+ files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/javascript"],
76
+ expected_end_state: "RSC packages, generated artifacts, and renderer configuration are consistent."
77
+ }
78
+ }.freeze
79
+
80
+ # A check section can contain several distinct diagnoses, so no section-wide
81
+ # repair command is currently safe. Keep fix_command nullable until a future
82
+ # diagnosis-specific contract can prove that one command repairs the exact finding.
83
+
84
+ module_function
85
+
86
+ def metadata(check_id)
87
+ CHECK_METADATA.fetch(check_id) do
88
+ raise ArgumentError, "Missing DoctorSchema::CHECK_METADATA entry for check id #{check_id.inspect}"
89
+ end
90
+ end
91
+
92
+ def docs_url(check_id)
93
+ "#{DOCS_URL}#check-id-#{check_id.tr('_', '-')}"
94
+ end
95
+
96
+ def summarize_statuses(statuses)
97
+ counts = statuses.tally
98
+ STATUSES.to_h { |status| [status.to_sym, counts.fetch(status, 0)] }
99
+ end
100
+
101
+ # Runtime validation keeps the emitted payload aligned with the documented
102
+ # contract without adding a runtime JSON-schema dependency to the gem.
103
+ def validate!(report)
104
+ required_keys!(report, %i[schema_version ror_version status checks summary], "report")
105
+ assert(report[:schema_version] == VERSION, "unsupported schema_version")
106
+ assert(report[:ror_version].is_a?(String), "ror_version must be a string")
107
+ assert(STATUSES.include?(report[:status]), "invalid report status")
108
+ assert(report[:checks].is_a?(Array), "checks must be an array")
109
+ report[:checks].each { |check| validate_check!(check) }
110
+ validate_unique_check_ids!(report[:checks])
111
+ validate_summary!(report)
112
+ report
113
+ end
114
+
115
+ def validate_check!(check)
116
+ required_keys!(check, %i[id title status severity message fix_command docs_url remediation details], "check")
117
+ assert(CHECK_METADATA.key?(check[:id]), "unknown check id #{check[:id].inspect}")
118
+ assert(STATUSES.include?(check[:status]), "invalid status for #{check[:id]}")
119
+ assert(SEVERITIES.include?(check[:severity]), "invalid severity for #{check[:id]}")
120
+ assert(check[:severity] == STATUS_SEVERITIES[check[:status]], "severity does not match status for #{check[:id]}")
121
+ validate_check_field_types!(check)
122
+ validate_details!(check)
123
+ validate_remediation!(check)
124
+ end
125
+
126
+ def validate_check_field_types!(check)
127
+ assert(check[:title].is_a?(String), "invalid title for #{check[:id]}")
128
+ assert(check[:message].nil? || check[:message].is_a?(String), "invalid message for #{check[:id]}")
129
+ assert(check[:fix_command].nil? || check[:fix_command].is_a?(String), "invalid fix_command for #{check[:id]}")
130
+ assert(check[:docs_url].is_a?(String), "invalid docs_url for #{check[:id]}")
131
+ end
132
+
133
+ def validate_details!(check)
134
+ assert(check[:details].is_a?(Array), "details must be an array for #{check[:id]}")
135
+ check[:details].each { |detail| validate_detail!(detail, check[:id]) }
136
+ end
137
+
138
+ def validate_detail!(detail, check_id)
139
+ required_keys!(detail, %i[level content], "detail for #{check_id}")
140
+ assert(DETAIL_LEVELS.include?(detail[:level]), "invalid detail level for #{check_id}")
141
+ assert(detail[:content].is_a?(String), "invalid detail content for #{check_id}")
142
+ end
143
+
144
+ def validate_remediation!(check)
145
+ remediation = check[:remediation]
146
+ if check[:status] == "pass"
147
+ assert(remediation.nil?, "passing check #{check[:id]} must not have remediation")
148
+ return
149
+ end
150
+
151
+ assert(remediation.is_a?(Hash), "non-passing check #{check[:id]} must have remediation")
152
+ required_keys!(remediation, %i[prompt files expected_end_state], "remediation")
153
+ assert(remediation[:prompt].is_a?(String) && !remediation[:prompt].empty?, "invalid remediation prompt")
154
+ assert(remediation[:files].is_a?(Array) && remediation[:files].all?(String), "invalid remediation files")
155
+ assert(remediation[:expected_end_state].is_a?(String), "invalid remediation expected_end_state")
156
+ end
157
+
158
+ def validate_unique_check_ids!(checks)
159
+ ids = checks.map { |check| check[:id] }
160
+ assert(ids.uniq.length == ids.length, "check ids must be unique")
161
+ end
162
+
163
+ def validate_summary!(report)
164
+ summary = report[:summary]
165
+ required_keys!(summary, %i[pass warn fail], "summary")
166
+ assert(summary.values.all? { |count| count.is_a?(Integer) && count >= 0 },
167
+ "summary values must be non-negative integers")
168
+
169
+ statuses = report[:checks].map { |check| check[:status] }
170
+ expected_summary = summarize_statuses(statuses)
171
+ assert(summary == expected_summary, "summary does not match checks")
172
+ assert(report[:status] == overall_status(statuses), "report status does not match checks")
173
+ end
174
+
175
+ def overall_status(statuses)
176
+ return "fail" if statuses.include?("fail")
177
+ return "warn" if statuses.include?("warn")
178
+
179
+ "pass"
180
+ end
181
+
182
+ def required_keys!(value, keys, label)
183
+ assert(value.is_a?(Hash), "#{label} must be an object")
184
+ missing = keys - value.keys
185
+ assert(missing.empty?, "#{label} missing keys: #{missing.join(', ')}")
186
+ end
187
+
188
+ def assert(condition, message)
189
+ raise ArgumentError, "Invalid doctor JSON report: #{message}" unless condition
190
+ end
191
+ end
192
+ end
@@ -30,9 +30,9 @@ module ReactOnRails
30
30
  config.after_initialize do
31
31
  next if Engine.skip_version_validation?
32
32
 
33
- Rails.logger.info "[React on Rails] Validating package version and compatibility..."
33
+ Rails.logger.debug "[React on Rails] Validating package version and compatibility..."
34
34
  VersionChecker.build.validate_version_and_package_compatibility!
35
- Rails.logger.info "[React on Rails] Package validation successful"
35
+ Rails.logger.debug "[React on Rails] Package validation successful"
36
36
  end
37
37
  end
38
38
 
@@ -7,6 +7,7 @@
7
7
  # 1. The white spacing in this file matters!
8
8
  # 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var
9
9
  require "react_on_rails/prerender_error"
10
+ require "react_on_rails/json_parse_error"
10
11
  require "react_on_rails/smart_error"
11
12
  require "addressable/uri"
12
13
  require "react_on_rails/utils"
@@ -904,6 +905,20 @@ module ReactOnRails
904
905
  )
905
906
  end
906
907
 
908
+ def raise_renderer_prerender_error(err, react_component_name, props, js_code)
909
+ prerender_error = ReactOnRails::PrerenderError.new(component_name: react_component_name,
910
+ # Sanitize as this might be browser logged
911
+ props: sanitized_props_string(props),
912
+ err:,
913
+ js_code:)
914
+ renderer_parse_error = err.is_a?(ReactOnRails::JsonParseError) ||
915
+ (defined?(ReactOnRails::LengthPrefixedParser::ParseError) &&
916
+ err.is_a?(ReactOnRails::LengthPrefixedParser::ParseError))
917
+ raise prerender_error, cause: nil if renderer_parse_error
918
+
919
+ raise prerender_error
920
+ end
921
+
907
922
  def rendering_error_from_result(json_result)
908
923
  rendering_error = json_result["renderingError"]
909
924
  return unless rendering_error.is_a?(Hash)
@@ -987,11 +1002,7 @@ module ReactOnRails
987
1002
  result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
988
1003
  rescue StandardError => err
989
1004
  # This error came from the renderer
990
- raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
991
- # Sanitize as this might be browser logged
992
- props: sanitized_props_string(props),
993
- err:,
994
- js_code:)
1005
+ raise_renderer_prerender_error(err, react_component_name, props, js_code)
995
1006
  end
996
1007
 
997
1008
  if render_options.streaming?
@@ -1005,11 +1016,7 @@ module ReactOnRails
1005
1016
 
1006
1017
  result.rescue do |err|
1007
1018
  # This error came from the renderer
1008
- raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
1009
- # Sanitize as this might be browser logged
1010
- props: sanitized_props_string(props),
1011
- err:,
1012
- js_code:)
1019
+ raise_renderer_prerender_error(err, react_component_name, props, js_code)
1013
1020
  end
1014
1021
  elsif result["hasErrors"] && render_options.raise_on_prerender_error
1015
1022
  raise_prerender_error(result, react_component_name, props, js_code)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "react_on_rails/lenient_json"
4
5
 
5
6
  module ReactOnRails
6
7
  # Parses the length-prefixed wire format used between Node renderer and Ruby.
@@ -11,6 +12,8 @@ module ReactOnRails
11
12
  # Used by both streaming (Pro) and non-streaming (OSS) paths.
12
13
  # Strict protocol parser — any format violation raises an error.
13
14
  class LengthPrefixedParser
15
+ class ParseError < ReactOnRails::Error; end
16
+
14
17
  # Keep aligned with ReactOnRailsPro::StreamRequest::CONTROL_MESSAGE_TYPES,
15
18
  # which routes these same control frames during bidirectional streaming.
16
19
  # MIRROR VALUES OF: react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb
@@ -28,11 +31,11 @@ module ReactOnRails
28
31
  results = []
29
32
  parser.feed(str.to_s.b) { |chunk| results << chunk }
30
33
  if results.empty?
31
- raise ReactOnRails::Error,
34
+ raise ParseError,
32
35
  "Malformed render result: expected exactly one length-prefixed chunk but found none"
33
36
  end
34
37
  if results.size > 1
35
- raise ReactOnRails::Error,
38
+ raise ParseError,
36
39
  "Malformed render result: expected exactly one length-prefixed chunk but found #{results.size}"
37
40
  end
38
41
  results.first
@@ -98,11 +101,7 @@ module ReactOnRails
98
101
  @buf = @buf.byteslice(idx + 1, @buf.bytesize - idx - 1)
99
102
 
100
103
  tab_idx = header.byteindex("\t".b)
101
- unless tab_idx
102
- header_str = header.force_encoding(Encoding::UTF_8).inspect
103
- raise ReactOnRails::Error,
104
- "Malformed length-prefixed header: missing tab separator in: #{header_str}"
105
- end
104
+ raise ParseError, "Malformed length-prefixed header: missing tab separator" unless tab_idx
106
105
 
107
106
  parse_length_prefixed_header(header, tab_idx)
108
107
  true
@@ -115,16 +114,18 @@ module ReactOnRails
115
114
  begin
116
115
  @content_len = Integer(len_hex, 16)
117
116
  rescue ArgumentError
118
- raise ReactOnRails::Error, "Invalid content length hex: #{len_hex.force_encoding(Encoding::UTF_8).inspect}"
117
+ raise ParseError, "Malformed length-prefixed header: invalid content length hex", cause: nil
119
118
  end
119
+ raise ParseError, "Malformed length-prefixed header: negative content length" if @content_len.negative?
120
120
 
121
121
  begin
122
- @metadata = JSON.parse(meta_json.force_encoding(Encoding::UTF_8))
123
- rescue JSON::ParserError => e
124
- meta_str = meta_json.force_encoding(Encoding::UTF_8).inspect
125
- raise ReactOnRails::Error,
126
- "Malformed length-prefixed header: invalid metadata JSON: #{meta_str} (#{e.message})"
122
+ # LenientJson tolerates lone UTF-16 surrogates that the JS renderer can emit
123
+ # (e.g. from truncating a string mid-emoji) which stock JSON.parse rejects. See #4710.
124
+ @metadata = LenientJson.parse(meta_json.force_encoding(Encoding::UTF_8))
125
+ rescue JSON::ParserError
126
+ raise ParseError, "Malformed length-prefixed header: invalid metadata JSON", cause: nil
127
127
  end
128
+ raise ParseError, "Malformed length-prefixed header: metadata JSON must be an object" unless @metadata.is_a?(Hash)
128
129
 
129
130
  @state = :content
130
131
  end
@@ -153,7 +154,7 @@ module ReactOnRails
153
154
  # "object" → JSON-serialized value (ServerRenderHash or null), needs JSON.parse
154
155
  # "string" → raw HTML string, used as-is
155
156
  payload_type = @metadata.delete("payloadType")
156
- @metadata["html"] = payload_type == "object" ? JSON.parse(raw_content) : raw_content
157
+ @metadata["html"] = payload_type == "object" ? parse_object_payload(raw_content) : raw_content
157
158
 
158
159
  result = @metadata
159
160
  @metadata = nil
@@ -161,5 +162,12 @@ module ReactOnRails
161
162
  yield result
162
163
  true
163
164
  end
165
+
166
+ def parse_object_payload(raw_content)
167
+ # See the metadata parse above: LenientJson repairs lone-surrogate escapes (#4710).
168
+ LenientJson.parse(raw_content)
169
+ rescue JSON::ParserError
170
+ raise ParseError, "Malformed length-prefixed object payload JSON", cause: nil
171
+ end
164
172
  end
165
173
  end
Binary file
@@ -5,7 +5,8 @@ require "rainbow"
5
5
  # rubocop:disable: Layout/IndentHeredoc
6
6
  module ReactOnRails
7
7
  class PrerenderError < ::ReactOnRails::Error
8
- MAX_ERROR_SNIPPET_TO_LOG = 1000
8
+ REDACTED_VALUE = "[REDACTED]"
9
+ SENSITIVE_CONTEXT_KEYS = %w[props js_code json].freeze
9
10
  # TODO: Consider remove providing original `err` as already have access to `self.cause`
10
11
  # http://blog.honeybadger.io/nested-errors-in-ruby-with-exception-cause/
11
12
  attr_reader :component_name, :err, :props, :js_code, :console_messages
@@ -15,8 +16,8 @@ module ReactOnRails
15
16
  js_code: nil, console_messages: nil)
16
17
  @component_name = component_name
17
18
  @err = err
18
- @props = props
19
- @js_code = js_code
19
+ @props = redacted_value(props)
20
+ @js_code = redacted_value(js_code)
20
21
  @console_messages = console_messages
21
22
 
22
23
  backtrace, message = calc_message(component_name, console_messages, err, js_code, props)
@@ -35,18 +36,43 @@ module ReactOnRails
35
36
  def to_error_context
36
37
  result = {
37
38
  component_name:,
38
- err:,
39
+ err: error_context_value(err),
39
40
  props:,
40
41
  js_code:,
41
42
  console_messages:
42
43
  }
43
44
 
44
- result.merge!(err.to_error_context) if err.respond_to?(:to_error_context)
45
+ if err.respond_to?(:to_error_context)
46
+ nested_context = err.to_error_context.reject { |key, _value| sensitive_nested_context_key?(key) }
47
+ result.merge!(nested_context)
48
+ end
45
49
  result
46
50
  end
47
51
 
48
52
  private
49
53
 
54
+ def renderer_parse_error?(error)
55
+ json_parse_error = defined?(JsonParseError) && error.is_a?(JsonParseError)
56
+ length_prefixed_parse_error = defined?(LengthPrefixedParser::ParseError) &&
57
+ error.is_a?(LengthPrefixedParser::ParseError)
58
+ json_parse_error || length_prefixed_parse_error
59
+ end
60
+
61
+ def safe_error_details(error)
62
+ return error.inspect unless renderer_parse_error?(error)
63
+
64
+ "#{error.class}: Renderer response could not be parsed"
65
+ end
66
+
67
+ def error_context_value(error)
68
+ renderer_parse_error?(error) ? safe_error_details(error) : error
69
+ end
70
+
71
+ def sensitive_nested_context_key?(key)
72
+ SENSITIVE_CONTEXT_KEYS.include?(key.to_s) ||
73
+ (renderer_parse_error?(err) && key.to_s == "original_error")
74
+ end
75
+
50
76
  # rubocop:disable Metrics/AbcSize
51
77
  def calc_message(component_name, console_messages, err, js_code, props)
52
78
  header = Rainbow("❌ React on Rails Server Rendering Error").red.bright
@@ -57,7 +83,7 @@ module ReactOnRails
57
83
  if err
58
84
  message << Rainbow("Error Details:").red.bright << "\n"
59
85
  message << <<~MSG
60
- #{err.inspect}
86
+ #{safe_error_details(err)}
61
87
 
62
88
  MSG
63
89
 
@@ -68,11 +94,11 @@ module ReactOnRails
68
94
 
69
95
  # Add props information
70
96
  message << Rainbow("Props:").blue.bright << "\n"
71
- message << "#{Utils.smart_trim(props, MAX_ERROR_SNIPPET_TO_LOG)}\n\n"
97
+ message << "#{redacted_value(props)}\n\n"
72
98
 
73
99
  # Add code snippet
74
100
  message << Rainbow("JavaScript Code:").blue.bright << "\n"
75
- message << "#{Utils.smart_trim(js_code, MAX_ERROR_SNIPPET_TO_LOG)}\n\n"
101
+ message << "#{redacted_value(js_code)}\n\n"
76
102
 
77
103
  if console_messages && console_messages.strip.present?
78
104
  message << Rainbow("Console Output:").magenta.bright << "\n"
@@ -90,6 +116,10 @@ module ReactOnRails
90
116
  # rubocop:enable Metrics/AbcSize
91
117
  end
92
118
 
119
+ def redacted_value(value)
120
+ value.nil? ? nil : REDACTED_VALUE
121
+ end
122
+
93
123
  # Formats `err.backtrace` for display, or returns nil when there are no frames.
94
124
  def formatted_backtrace(err)
95
125
  error_backtrace = err.backtrace
@@ -123,7 +123,7 @@ module ReactOnRails
123
123
  end
124
124
 
125
125
  def html_options
126
- options[:html_options].to_h
126
+ options[:html_options].to_h.dup
127
127
  end
128
128
 
129
129
  def prerender