product_tours 1.0.0 → 1.2.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/AGENTS.md +2 -2
- data/CHANGELOG.md +45 -1
- data/README.md +33 -6
- data/Rakefile +9 -1
- data/SECURITY.md +2 -2
- data/app/controllers/product_tours/interactions_controller.rb +20 -2
- data/app/controllers/product_tours/tours_controller.rb +3 -0
- data/app/models/product_tours/tour.rb +59 -2
- data/app/views/product_tours/tours/_form.html.erb +15 -0
- data/app/views/product_tours/tours/_tour_panel.html.erb +6 -0
- data/config/locales/product_tours.ar.yml +1 -0
- data/config/locales/product_tours.bg.yml +1 -0
- data/config/locales/product_tours.bn.yml +1 -0
- data/config/locales/product_tours.de.yml +1 -0
- data/config/locales/product_tours.el.yml +1 -0
- data/config/locales/product_tours.en.yml +1 -0
- data/config/locales/product_tours.es.yml +1 -0
- data/config/locales/product_tours.fr.yml +1 -0
- data/config/locales/product_tours.hi.yml +1 -0
- data/config/locales/product_tours.hr.yml +1 -0
- data/config/locales/product_tours.id.yml +1 -0
- data/config/locales/product_tours.it.yml +1 -0
- data/config/locales/product_tours.ja.yml +1 -0
- data/config/locales/product_tours.ko.yml +1 -0
- data/config/locales/product_tours.lb.yml +1 -0
- data/config/locales/product_tours.nl.yml +1 -0
- data/config/locales/product_tours.pl.yml +1 -0
- data/config/locales/product_tours.pt.yml +1 -0
- data/config/locales/product_tours.ro.yml +1 -0
- data/config/locales/product_tours.ru.yml +1 -0
- data/config/locales/product_tours.th.yml +1 -0
- data/config/locales/product_tours.tr.yml +1 -0
- data/config/locales/product_tours.uk.yml +1 -0
- data/config/locales/product_tours.ur.yml +1 -0
- data/config/locales/product_tours.vi.yml +1 -0
- data/config/locales/product_tours.zh-CN.yml +1 -0
- data/docs/ractors.md +138 -0
- data/lib/generators/product_tours/install/templates/create_product_tours_tours.rb.tt +2 -0
- data/lib/generators/product_tours/upgrade/templates/add_player_media_to_product_tours_tours.rb.tt +17 -0
- data/lib/generators/product_tours/upgrade/upgrade_generator.rb +29 -0
- data/lib/product_tours/engine.rb +12 -0
- data/lib/product_tours/ractor_support.rb +519 -0
- data/lib/product_tours/version.rb +1 -1
- data/lib/product_tours/widget.js +13 -1
- data/lib/product_tours/widget.rb +3 -3
- data/lib/product_tours.rb +1 -0
- metadata +6 -2
data/docs/ractors.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Rails edge and experimental Ractor hosting
|
|
2
|
+
|
|
3
|
+
The regular CI matrix covers Rails 7.1, 7.2, 8.0 and 8.1. A required Rails
|
|
4
|
+
edge job runs the same suite against `rails/rails` main. A separate job boots
|
|
5
|
+
an eager-loaded host, calls `Rails.application.ractorize!`, and sends real
|
|
6
|
+
Rack requests through the mounted engine in non-main Ractors, including
|
|
7
|
+
simultaneous requests. The Ractor checks run in separate processes because
|
|
8
|
+
Ractorization freezes Rails; they never freeze the ordinary test application.
|
|
9
|
+
|
|
10
|
+
## Host prerequisites
|
|
11
|
+
|
|
12
|
+
Ractor hosting currently requires Ruby 4.0+, Rails main, Rack main's
|
|
13
|
+
`rack/ractorize`, and the unreleased [I18n Ractor support](https://github.com/ruby-i18n/i18n/pull/741).
|
|
14
|
+
`gemfiles/rails_ractor.gemfile` records the exact I18n revision tested. These
|
|
15
|
+
are development/test dependencies, not new runtime dependencies for released
|
|
16
|
+
Rails. The gem's supported dependency range remains Rails >= 7.1 and < 9.
|
|
17
|
+
|
|
18
|
+
For a host experimenting with Ractor hosting, the tested upstream overrides are:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "rails", github: "rails/rails", branch: "main"
|
|
22
|
+
gem "rack", github: "rack/rack", branch: "main"
|
|
23
|
+
gem "i18n", github: "Shopify/i18n", ref: "58aa1dcd3e04c34831e1c2ec06c83391559c7c91"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Follow Rails' own Ractor hosting requirements: disable reloading, eager-load
|
|
27
|
+
application code, use a shareable logger/cache and a Ractor-compatible job
|
|
28
|
+
adapter. Before application classes load, enable Rails' callback isolation:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "active_support/ractors"
|
|
32
|
+
ActiveSupport::Ractors.unshareable_proc_action = :raise
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Our isolated fixture uses `ActiveSupport::Ractors::Logger`, a null
|
|
36
|
+
cache, and (where jobs are loaded) `:inline`. Its separate shared counter proves
|
|
37
|
+
that public submission limits remain effective across worker Ractors. The
|
|
38
|
+
ordinary dummy application's `:test` job adapter is unchanged.
|
|
39
|
+
|
|
40
|
+
The engine prepares itself automatically on `before_ractorize`; ordinary Rails
|
|
41
|
+
boots do not freeze its configuration or load the experimental I18n extension.
|
|
42
|
+
An unavailable I18n Ractor extension produces a startup error explaining the
|
|
43
|
+
prerequisite. Rails edge and Ractor hosting remain experimental: this is not a
|
|
44
|
+
promise that every third-party host middleware, database driver, storage
|
|
45
|
+
service, mail adapter or Action Cable adapter is Ractor safe.
|
|
46
|
+
|
|
47
|
+
## Configuration and callbacks
|
|
48
|
+
|
|
49
|
+
Finish configuring ProductTours before calling `ractorize!`. The engine publishes
|
|
50
|
+
a frozen copy of the configuration and warms/freezes widget JS, CSS and content
|
|
51
|
+
fingerprints. Changes after Ractorization require a process restart. Arrays and
|
|
52
|
+
hashes supplied by the host are copied before freezing.
|
|
53
|
+
|
|
54
|
+
Callbacks are isolated with Ruby's `Ractor.shareable_lambda` or
|
|
55
|
+
`Ractor.shareable_proc`, preserving lambda/proc arity. They continue to receive
|
|
56
|
+
the same request, user, record or event arguments and run on each request.
|
|
57
|
+
Isolated callbacks use arguments and constants; their implicit initializer
|
|
58
|
+
`self` becomes nil, following Rails' isolated callback convention.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
ProductTours.configure do |config|
|
|
62
|
+
config.enabled = ->(request) { request.get_header("HTTP_X_WIDGET") != "off" }
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Immutable captured values must already be shareable:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
expected_host = "app.example.com".freeze
|
|
70
|
+
ProductTours.configure do |config|
|
|
71
|
+
config.enabled = ->(request) { request.host == expected_host }
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Mutable captures (an array collecting notifications, an unfrozen captured
|
|
76
|
+
string, or reassigned outer locals) cannot be shared. The engine fails at
|
|
77
|
+
startup with `ProductTours.config.<setting>` in the error rather than failing on
|
|
78
|
+
a visitor's request. Use request-local state and external persistence for
|
|
79
|
+
side effects. A pre-isolated callback made with an explicit shareable `self:`
|
|
80
|
+
keeps that receiver. Stateful callable objects must be copyable/shareable;
|
|
81
|
+
custom objects remain subject to Ruby's Ractor rules.
|
|
82
|
+
|
|
83
|
+
## Scope and cost
|
|
84
|
+
|
|
85
|
+
Some Rails main paths are still being converted to Ractors. The engine warms
|
|
86
|
+
model attributes and controller names, prepares ETag callbacks, and supplies
|
|
87
|
+
narrow bridges for mounted URL generation, cache freshness, digest/key
|
|
88
|
+
operations, GlobalID URI decoding and boot-time attribute decorators. Those bridges preserve the
|
|
89
|
+
original operation on the main Ractor; requests, authorization callbacks,
|
|
90
|
+
rendering and persistence orchestration execute in their worker Ractors.
|
|
91
|
+
Adapter name quoting delegates to its original implementation on the main Ractor,
|
|
92
|
+
which preserves escaping and avoids process-global mutable caches in workers.
|
|
93
|
+
Disk-backed uploads use Ractor-local storage clients and plain temporary files
|
|
94
|
+
with explicit cleanup, because Tempfile's generated delegation methods cannot
|
|
95
|
+
be called from another Ractor. MIME registrations must finish before
|
|
96
|
+
Ractorization, when Marcel's MIME tables become immutable. Eager model schema
|
|
97
|
+
preparation adds startup database work.
|
|
98
|
+
The bridges are internal compatibility code, add coordination overhead, and
|
|
99
|
+
must be reviewed as upstream Rails evolves. No throughput improvement is claimed.
|
|
100
|
+
|
|
101
|
+
## Run the checks
|
|
102
|
+
|
|
103
|
+
Use the Ruby configured in `mise.toml`, never macOS's system Ruby.
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install
|
|
107
|
+
BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle exec rake test
|
|
108
|
+
BUNDLE_GEMFILE=gemfiles/rails_ractor.gemfile bundle install
|
|
109
|
+
BUNDLE_GEMFILE=gemfiles/rails_ractor.gemfile bundle exec rake test:ractor
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The request fixture creates and removes its own temporary database and host;
|
|
113
|
+
it does not reuse or freeze `test/dummy`. The shared schema lives in
|
|
114
|
+
`test/support/schema.rb` so the ordinary and Ractor suites exercise the same
|
|
115
|
+
record shape. Successful checks establish the tested SQLite host and explicit
|
|
116
|
+
request paths, not all combinations of host integrations.
|
|
117
|
+
|
|
118
|
+
The media job also runs with `RACTOR_MEDIA=1`, exercising private disk-backed
|
|
119
|
+
Active Storage downloads through the engine's authorization gate. Storage
|
|
120
|
+
services are initialized per Ractor, and streamed responses use a thread local
|
|
121
|
+
to that Ractor. Livechat additionally runs with `RACTOR_CABLE=1`, checking the
|
|
122
|
+
signed stream token and broadcasts delegated to the main Action Cable server.
|
|
123
|
+
Other storage services and production Cable adapters need host-specific QA.
|
|
124
|
+
|
|
125
|
+
## Action Text and Nokogiri
|
|
126
|
+
|
|
127
|
+
When Action Text is loaded, Nokogiri's native parser refuses to run outside the
|
|
128
|
+
main Ractor. Product Tours therefore delegates its HTML dashboard/editor paths
|
|
129
|
+
and `/widget/tour` responses to the main Rails application. It keeps the original
|
|
130
|
+
Action Text sanitizer, rendering, callbacks and transactions intact, including
|
|
131
|
+
rich-text editing. Request bodies and responses for those paths are buffered;
|
|
132
|
+
large editor uploads therefore have an extra memory cost. These paths do not
|
|
133
|
+
run in parallel. Static assets, lifecycle signals and gated media downloads
|
|
134
|
+
continue in worker Ractors. A host without Action Text does not use this fallback.
|
|
135
|
+
The Ractor media CI job covers both video uploads and rich-text rendering.
|
|
136
|
+
|
|
137
|
+
The Product Tours Ractor matrix separately checks a minimal host, an Active
|
|
138
|
+
Storage host without Action Text (`RACTOR_TEXT=0`), and a host with both engines.
|
|
@@ -9,6 +9,8 @@ class CreateProductToursTours < ActiveRecord::Migration<%= migration_version %>
|
|
|
9
9
|
t.string :title, null: false
|
|
10
10
|
t.text :video_url
|
|
11
11
|
t.json :video_metadata, null: false, default: {}
|
|
12
|
+
t.text :poster_url
|
|
13
|
+
t.text :captions_url
|
|
12
14
|
t.string :action_label
|
|
13
15
|
t.text :action_url
|
|
14
16
|
t.string :action_tour_key
|
data/lib/generators/product_tours/upgrade/templates/add_player_media_to_product_tours_tours.rb.tt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddPlayerMediaToProductToursTours < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
COLUMNS = %i[poster_url captions_url].freeze
|
|
5
|
+
|
|
6
|
+
def up
|
|
7
|
+
COLUMNS.each do |column|
|
|
8
|
+
add_column :product_tours_tours, column, :text unless column_exists?(:product_tours_tours, column)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def down
|
|
13
|
+
COLUMNS.each do |column|
|
|
14
|
+
remove_column :product_tours_tours, column if column_exists?(:product_tours_tours, column)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
require_relative '../migration_helpers'
|
|
6
|
+
|
|
7
|
+
module ProductTours
|
|
8
|
+
module Generators
|
|
9
|
+
# Brings an install created by an earlier version up to the current schema.
|
|
10
|
+
# Every migration it writes checks before it changes anything, so running it
|
|
11
|
+
# on an app that is already current is harmless.
|
|
12
|
+
class UpgradeGenerator < Rails::Generators::Base
|
|
13
|
+
include ActiveRecord::Generators::Migration
|
|
14
|
+
include MigrationHelpers
|
|
15
|
+
|
|
16
|
+
source_root File.expand_path('templates', __dir__)
|
|
17
|
+
desc 'Adds the columns newer product_tours versions use to an existing install.'
|
|
18
|
+
|
|
19
|
+
def create_player_media_migration
|
|
20
|
+
migration_template 'add_player_media_to_product_tours_tours.rb.tt',
|
|
21
|
+
'db/migrate/add_player_media_to_product_tours_tours.rb', skip: true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def post_upgrade
|
|
25
|
+
say "\nRun `bin/rails db:migrate` to finish the product_tours upgrade.", :green
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/product_tours/engine.rb
CHANGED
|
@@ -4,8 +4,20 @@ require 'rails'
|
|
|
4
4
|
|
|
5
5
|
module ProductTours
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
|
+
initializer 'product_tours.ractor_middleware' do |app|
|
|
8
|
+
app.middleware.insert_before 0, RactorSupport::Middleware
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
isolate_namespace ProductTours
|
|
8
12
|
|
|
13
|
+
initializer 'product_tours.ractorize', before: 'action_controller.ractorize' do
|
|
14
|
+
if Rails::Application.method_defined?(:ractorize!)
|
|
15
|
+
Engine.routes.singleton_class.prepend(RactorSupport::MountedRoutes)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ActiveSupport.on_load(:before_ractorize) { ProductTours::RactorSupport.prepare! }
|
|
19
|
+
end
|
|
20
|
+
|
|
9
21
|
initializer 'product_tours.helpers' do
|
|
10
22
|
ActiveSupport.on_load(:action_view) do
|
|
11
23
|
include ProductTours::WidgetHelper
|