universal_renderer 0.5.2 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +410 -178
- data/lib/generators/universal_renderer/install_generator.rb +61 -3
- data/lib/generators/universal_renderer/templates/initializer.rb +8 -5
- data/lib/generators/universal_renderer/templates/ssr/config.ts +40 -0
- data/lib/generators/universal_renderer/templates/ssr/dev.ts +9 -0
- data/lib/generators/universal_renderer/templates/ssr/globals.ts +141 -0
- data/lib/generators/universal_renderer/templates/ssr/server.ts +24 -0
- data/lib/generators/universal_renderer/templates/ssr.rake +27 -0
- data/lib/generators/universal_renderer/templates/vite.config.ssr.mts +13 -0
- data/lib/generators/universal_renderer/templates/web +94 -0
- data/lib/universal_renderer/client/base.rb +99 -44
- data/lib/universal_renderer/client/stream/error_logger.rb +25 -3
- data/lib/universal_renderer/client/stream/execution.rb +27 -1
- data/lib/universal_renderer/client/stream/setup.rb +6 -1
- data/lib/universal_renderer/client/stream.rb +49 -12
- data/lib/universal_renderer/configuration.rb +56 -6
- data/lib/universal_renderer/engine.rb +9 -2
- data/lib/universal_renderer/instrumentation.rb +47 -0
- data/lib/universal_renderer/renderable.rb +220 -86
- data/lib/universal_renderer/ssr/helpers.rb +82 -57
- data/lib/universal_renderer/ssr/response.rb +6 -1
- data/lib/universal_renderer/ssr/scrubber.rb +119 -30
- data/lib/universal_renderer/version.rb +1 -1
- data/lib/universal_renderer.rb +1 -0
- metadata +9 -1
|
@@ -8,7 +8,8 @@ module UniversalRenderer
|
|
|
8
8
|
http_client,
|
|
9
9
|
http_post_request,
|
|
10
10
|
response,
|
|
11
|
-
stream_uri
|
|
11
|
+
stream_uri,
|
|
12
|
+
&on_failure
|
|
12
13
|
)
|
|
13
14
|
success = false
|
|
14
15
|
chunks_written = false
|
|
@@ -28,11 +29,22 @@ module UniversalRenderer
|
|
|
28
29
|
end
|
|
29
30
|
success = true
|
|
30
31
|
else
|
|
32
|
+
error =
|
|
33
|
+
StandardError.new(
|
|
34
|
+
"SSR stream server responded with " \
|
|
35
|
+
"#{node_res.code} #{node_res.message}"
|
|
36
|
+
)
|
|
31
37
|
UniversalRenderer.log do |log|
|
|
32
38
|
log.error(
|
|
33
39
|
"SSR stream server at #{stream_uri} responded with #{node_res.code} #{node_res.message}."
|
|
34
40
|
)
|
|
35
41
|
end
|
|
42
|
+
on_failure&.call(
|
|
43
|
+
error,
|
|
44
|
+
outcome: :http_error,
|
|
45
|
+
status: node_res.code.to_i,
|
|
46
|
+
stage: :response
|
|
47
|
+
)
|
|
36
48
|
end
|
|
37
49
|
end
|
|
38
50
|
rescue StandardError => e
|
|
@@ -46,6 +58,11 @@ module UniversalRenderer
|
|
|
46
58
|
# current persistent connection. Net::HTTP can also raise after its
|
|
47
59
|
# request block returns while it finalizes that response body.
|
|
48
60
|
HttpPool.close(upstream_connection) if upstream_connection
|
|
61
|
+
on_failure&.call(
|
|
62
|
+
e,
|
|
63
|
+
outcome: failure_outcome(e),
|
|
64
|
+
stage: :transfer
|
|
65
|
+
)
|
|
49
66
|
success = chunks_written
|
|
50
67
|
ensure
|
|
51
68
|
response.stream.close if success && !response.stream.closed?
|
|
@@ -53,6 +70,15 @@ module UniversalRenderer
|
|
|
53
70
|
|
|
54
71
|
success
|
|
55
72
|
end
|
|
73
|
+
|
|
74
|
+
def self.failure_outcome(error)
|
|
75
|
+
return :timeout if error.is_a?(Net::OpenTimeout) ||
|
|
76
|
+
error.is_a?(Net::ReadTimeout)
|
|
77
|
+
|
|
78
|
+
:error
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private_class_method :failure_outcome
|
|
56
82
|
end
|
|
57
83
|
end
|
|
58
84
|
end
|
|
@@ -14,7 +14,12 @@ module UniversalRenderer
|
|
|
14
14
|
raise ArgumentError, "SSR URL is not configured." if config.url.blank?
|
|
15
15
|
|
|
16
16
|
parsed_ssr_url = URI.parse(config.url)
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
# URI.join with a relative path replaces the base URL's last path
|
|
19
|
+
# segment; see Client::Base.absolute_path.
|
|
20
|
+
stream_path = config.stream_path.to_s
|
|
21
|
+
stream_path = "/#{stream_path}" unless stream_path.start_with?("/")
|
|
22
|
+
stream_uri = URI.join(parsed_ssr_url, stream_path)
|
|
18
23
|
|
|
19
24
|
http = HttpPool.client(stream_uri, config.timeout)
|
|
20
25
|
|
|
@@ -7,9 +7,8 @@ require_relative "stream/setup"
|
|
|
7
7
|
module UniversalRenderer
|
|
8
8
|
module Client
|
|
9
9
|
class Stream
|
|
10
|
-
extend
|
|
11
|
-
|
|
12
|
-
extend Setup
|
|
10
|
+
# The three modules below define only singleton methods, so `extend` would
|
|
11
|
+
# import nothing while reading as though it had. Call sites name the module.
|
|
13
12
|
|
|
14
13
|
# Orchestrates the streaming process for server-side rendering.
|
|
15
14
|
#
|
|
@@ -19,9 +18,20 @@ module UniversalRenderer
|
|
|
19
18
|
# @param response [ActionDispatch::Response] The Rails response object to stream to.
|
|
20
19
|
# @return [Boolean] True if streaming was initiated, false otherwise.
|
|
21
20
|
def self.call(url, props, template, response)
|
|
21
|
+
Instrumentation.instrument(url: url, mode: :streaming) do |event|
|
|
22
|
+
succeeded = perform(url, props, template, response, event)
|
|
23
|
+
event[:outcome] = :error if !succeeded && event[:outcome] == :ok
|
|
24
|
+
succeeded
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.perform(url, props, template, response, event = {})
|
|
29
|
+
initialize_event(event, url)
|
|
30
|
+
|
|
22
31
|
config = UniversalRenderer.config
|
|
23
32
|
|
|
24
33
|
unless Setup.ensure_ssr_server_url_configured?(config)
|
|
34
|
+
event[:outcome] = :not_configured
|
|
25
35
|
UniversalRenderer.log do |log|
|
|
26
36
|
log.warn(
|
|
27
37
|
"Stream: SSR URL (config.url) is not configured. Falling back."
|
|
@@ -43,14 +53,14 @@ module UniversalRenderer
|
|
|
43
53
|
|
|
44
54
|
full_ssr_url_for_log = actual_stream_uri.to_s # Update for more specific logging
|
|
45
55
|
rescue URI::InvalidURIError => e
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
end
|
|
56
|
+
event[:outcome] = :error
|
|
57
|
+
event[:error] = e
|
|
58
|
+
ErrorLogger.log_setup_error(e, config.url.to_s, event)
|
|
51
59
|
return false
|
|
52
60
|
rescue StandardError => e
|
|
53
|
-
|
|
61
|
+
event[:outcome] = :error
|
|
62
|
+
event[:error] = e
|
|
63
|
+
ErrorLogger.log_setup_error(e, full_ssr_url_for_log, event)
|
|
54
64
|
return false
|
|
55
65
|
end
|
|
56
66
|
|
|
@@ -59,7 +69,14 @@ module UniversalRenderer
|
|
|
59
69
|
http_post_request,
|
|
60
70
|
response,
|
|
61
71
|
stream_uri_obj
|
|
62
|
-
)
|
|
72
|
+
) do |error, details|
|
|
73
|
+
event.merge!(details)
|
|
74
|
+
event[:error] = error
|
|
75
|
+
Instrumentation.report(
|
|
76
|
+
error,
|
|
77
|
+
event.merge(target: stream_uri_obj.to_s)
|
|
78
|
+
)
|
|
79
|
+
end
|
|
63
80
|
rescue Errno::ECONNREFUSED,
|
|
64
81
|
Errno::EHOSTUNREACH,
|
|
65
82
|
Net::OpenTimeout,
|
|
@@ -69,21 +86,41 @@ module UniversalRenderer
|
|
|
69
86
|
uri_str_for_conn_error =
|
|
70
87
|
stream_uri_obj ? stream_uri_obj.to_s : full_ssr_url_for_log
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
event[:outcome] = failure_outcome(e)
|
|
90
|
+
event[:error] = e
|
|
91
|
+
ErrorLogger.log_connection_error(e, uri_str_for_conn_error, event)
|
|
73
92
|
|
|
74
93
|
false
|
|
75
94
|
rescue StandardError => e
|
|
76
95
|
uri_str_for_unexpected_error =
|
|
77
96
|
stream_uri_obj ? stream_uri_obj.to_s : full_ssr_url_for_log
|
|
78
97
|
|
|
98
|
+
event[:outcome] = :error
|
|
99
|
+
event[:error] = e
|
|
79
100
|
ErrorLogger.log_unexpected_error(
|
|
80
101
|
e,
|
|
81
102
|
uri_str_for_unexpected_error,
|
|
82
|
-
"Stream: Unexpected error during SSR stream process"
|
|
103
|
+
"Stream: Unexpected error during SSR stream process",
|
|
104
|
+
event
|
|
83
105
|
)
|
|
84
106
|
|
|
85
107
|
false
|
|
86
108
|
end
|
|
109
|
+
|
|
110
|
+
def self.initialize_event(event, url)
|
|
111
|
+
event[:url] ||= url
|
|
112
|
+
event[:mode] ||= :streaming
|
|
113
|
+
event[:outcome] ||= :ok
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.failure_outcome(error)
|
|
117
|
+
return :timeout if error.is_a?(Net::OpenTimeout) ||
|
|
118
|
+
error.is_a?(Net::ReadTimeout)
|
|
119
|
+
|
|
120
|
+
:error
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private_class_method :initialize_event, :failure_outcome
|
|
87
124
|
end
|
|
88
125
|
end
|
|
89
126
|
end
|
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
module UniversalRenderer
|
|
4
4
|
# Configuration for UniversalRenderer.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# responsibility, done in the initializer (see the generated
|
|
9
|
-
# config/initializers/universal_renderer.rb). The documented env-var
|
|
10
|
-
# convention is the `UNIVERSAL_RENDERER_*` prefix.
|
|
6
|
+
# Plain Ruby defaults only. Binding these to ENV is the host application's job,
|
|
7
|
+
# done in the generated initializer.
|
|
11
8
|
class Configuration
|
|
12
9
|
# HTTP client options.
|
|
13
10
|
class Http
|
|
@@ -18,13 +15,66 @@ module UniversalRenderer
|
|
|
18
15
|
end
|
|
19
16
|
end
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
# Origin of the SSR service, e.g. "http://localhost:3001".
|
|
19
|
+
attr_accessor :url
|
|
20
|
+
|
|
21
|
+
# Open and read timeout, in seconds, for every request to the SSR service.
|
|
22
|
+
#
|
|
23
|
+
# Keep this above the renderer's own `renderTimeout` (2.5s by default). A
|
|
24
|
+
# render that outlives this timeout keeps its concurrency slot until it
|
|
25
|
+
# finishes, so the renderer must give up first.
|
|
26
|
+
attr_accessor :timeout
|
|
27
|
+
|
|
28
|
+
# Path the blocking renderer is mounted at. Must match the `paths.render`
|
|
29
|
+
# option given to `createServer`, whose default mounts `/` and `/static`.
|
|
30
|
+
#
|
|
31
|
+
# Defaults to nil, meaning the path already in `url`. A relative value is
|
|
32
|
+
# treated as absolute, since joining it relatively would replace the last
|
|
33
|
+
# path segment of `url`.
|
|
34
|
+
attr_accessor :render_path
|
|
35
|
+
|
|
36
|
+
# Path the streaming renderer is mounted at on the SSR service. Must match
|
|
37
|
+
# the `paths.stream` option given to `createServer` in the NPM package.
|
|
38
|
+
# Normalized the same way as `render_path`.
|
|
39
|
+
attr_accessor :stream_path
|
|
40
|
+
|
|
41
|
+
# Whether `ssr_head`/`ssr_body` run the renderer's HTML through Loofah.
|
|
42
|
+
#
|
|
43
|
+
# {SSR::Scrubber} is a blocklist, so this is defense in depth over HTML your
|
|
44
|
+
# own renderer produced, not a boundary against attacker-controlled markup.
|
|
45
|
+
# It also parses and rewrites the whole document on every request. On by
|
|
46
|
+
# default, because the cost is bounded and the mistake it catches is not.
|
|
47
|
+
attr_accessor :sanitize
|
|
48
|
+
|
|
49
|
+
# Scrubber instance used when `sanitize` is true. Defaults to
|
|
50
|
+
# {UniversalRenderer::SSR::Scrubber}; assign your own Loofah::Scrubber to
|
|
51
|
+
# widen or narrow what survives.
|
|
52
|
+
attr_accessor :scrubber
|
|
53
|
+
|
|
54
|
+
# Whether the Rails engine includes {UniversalRenderer::Renderable} into
|
|
55
|
+
# every ActionController::Base descendant. Turn it off and include the
|
|
56
|
+
# concern only in the controllers that render server-side.
|
|
57
|
+
attr_accessor :auto_include
|
|
58
|
+
|
|
59
|
+
# Optional callable invoked as `call(error, context)` when a configured
|
|
60
|
+
# render fails, where `context` carries at least `:url` and `:outcome`. A
|
|
61
|
+
# missing `url` reports `:not_configured` through the notification only.
|
|
62
|
+
#
|
|
63
|
+
# Errors are always logged; this exists so they can also reach an exception
|
|
64
|
+
# tracker, since every failure falls back to client rendering silently.
|
|
65
|
+
attr_accessor :on_error
|
|
66
|
+
|
|
22
67
|
attr_reader :http
|
|
23
68
|
|
|
24
69
|
def initialize
|
|
25
70
|
@url = nil
|
|
26
71
|
@timeout = 3
|
|
72
|
+
@render_path = nil
|
|
27
73
|
@stream_path = "/stream"
|
|
74
|
+
@sanitize = true
|
|
75
|
+
@scrubber = nil
|
|
76
|
+
@auto_include = true
|
|
77
|
+
@on_error = nil
|
|
28
78
|
@http = Http.new
|
|
29
79
|
end
|
|
30
80
|
end
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
module UniversalRenderer
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# After config initializers, so `config.auto_include = false` is honoured
|
|
4
|
+
# whenever ActionController::Base happens to load.
|
|
5
|
+
initializer "universal_renderer.renderable",
|
|
6
|
+
after: :load_config_initializers do
|
|
7
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
8
|
+
if UniversalRenderer.config.auto_include
|
|
9
|
+
include UniversalRenderer::Renderable
|
|
10
|
+
end
|
|
11
|
+
end
|
|
5
12
|
end
|
|
6
13
|
end
|
|
7
14
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UniversalRenderer
|
|
4
|
+
# Every SSR failure falls back to client-side rendering silently, which is
|
|
5
|
+
# right for availability and useless for operations. These two hooks are the
|
|
6
|
+
# signal that SSR stopped working.
|
|
7
|
+
module Instrumentation
|
|
8
|
+
# ActiveSupport::Notifications event name. Subscribe to record hit rate and
|
|
9
|
+
# latency:
|
|
10
|
+
#
|
|
11
|
+
# ActiveSupport::Notifications.subscribe("render.universal_renderer") do |event|
|
|
12
|
+
# StatsD.timing("ssr.duration", event.duration, tags: ["outcome:#{event.payload[:outcome]}"])
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Payload keys: `:url`, `:mode` (`:blocking` or `:streaming`), `:outcome`
|
|
16
|
+
# (`:ok`, `:not_configured`, `:http_error`, `:timeout`, or `:error`),
|
|
17
|
+
# and `:status` / `:error` where applicable.
|
|
18
|
+
NOTIFICATION = "render.universal_renderer"
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Instruments one render attempt. The block receives the mutable payload so
|
|
23
|
+
# it can record the outcome it reached.
|
|
24
|
+
def instrument(url:, mode:)
|
|
25
|
+
payload = { url: url, mode: mode, outcome: :ok }
|
|
26
|
+
|
|
27
|
+
ActiveSupport::Notifications.instrument(NOTIFICATION, payload) do
|
|
28
|
+
yield payload
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Hands an error to `config.on_error`, if one is configured. A raising
|
|
33
|
+
# callback must not turn a degraded render into a failed request.
|
|
34
|
+
def report(error, context)
|
|
35
|
+
callback = UniversalRenderer.config.on_error
|
|
36
|
+
return unless callback
|
|
37
|
+
|
|
38
|
+
callback.call(error, context)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
UniversalRenderer.log do |log|
|
|
41
|
+
log.error(
|
|
42
|
+
"on_error callback raised: #{e.class.name} - #{e.message}"
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|