demo_mode 3.4.0 → 3.5.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 +62 -22
- data/lib/demo_mode/clever_sequence/lower_bound_finder.rb +30 -0
- data/lib/demo_mode/clever_sequence/postgres_backend.rb +98 -0
- data/lib/demo_mode/clever_sequence.rb +15 -29
- data/lib/demo_mode/persona.rb +8 -6
- data/lib/demo_mode/version.rb +1 -1
- data/lib/generators/templates/initializer.rb +8 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a112b6f295027661f77ad3e708d373f6e24483ee562aa23307787c29f5cb45a9
|
|
4
|
+
data.tar.gz: 61c4948ea423a70295d48ed8b62a2d7de6359efa30560a8b40faa6831e2ada3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85fc9433912c6d7b9272cb9e7c526b9fdb7f3326784d6b65ba0b42dd9b9b022f643b79c7f7f5a299af5ee9bddf6c3594414a010fb1dc2bf670642a25b4c363c2
|
|
7
|
+
data.tar.gz: e05fed5948d72ef9cdd31bf01be8a9589648cdee0da4c6e24c080e1f82fab3cf2c669b6eb47dab470e730b8adbce943cd85c76c3c160f3bc1ed6bc458aaffad3
|
data/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
Demo Mode
|
|
2
|
-
|
|
1
|
+
# Demo Mode
|
|
2
|
+
|
|
3
3
|
[](https://rubygems.org/gems/demo_mode)
|
|
4
4
|
[](https://github.com/Betterment/demo_mode/actions/workflows/tests.yml)
|
|
5
5
|
|
|
@@ -35,22 +35,23 @@ To learn more about how we use `demo_mode` at **Betterment**, check out :sparkle
|
|
|
35
35
|
|
|
36
36
|
## Table of Contents
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
- [Getting Started](#getting-started)
|
|
39
|
+
- [Installation](#installation)
|
|
40
|
+
- [App-Specific Setup](#app-specific-setup)
|
|
41
|
+
- [Defining Personas](#defining-personas)
|
|
42
|
+
- [Customizing the Design](#customizing-the-design)
|
|
43
|
+
- [Optional Features](#optional-features)
|
|
44
|
+
- [The "Sign Up" Link](#the-sign-up-link)
|
|
45
|
+
- [The "Display Credentials" feature](#the-display-credentials-feature)
|
|
46
|
+
- [Developer CLI](#developer-cli)
|
|
47
|
+
- [Callbacks](#callbacks)
|
|
48
|
+
- [Non-User Personas](#non-user-personas)
|
|
49
|
+
- [FactoryBot `sequence` extension](#factorybot-sequence-extension)
|
|
50
|
+
- [Database-backed sequences](#database-backed-sequences)
|
|
51
|
+
- [Deploying a demo environment to the cloud](#deploying-a-demo-environment-to-the-cloud)
|
|
52
|
+
- [How to avoid breaking your new "demo" env](#how-to-avoid-breaking-your-new-demo-env)
|
|
53
|
+
- [How to Contribute](#how-to-contribute)
|
|
54
|
+
- [Suggested Workflow](#suggested-workflow)
|
|
54
55
|
|
|
55
56
|
## Getting Started
|
|
56
57
|
|
|
@@ -538,6 +539,38 @@ end
|
|
|
538
539
|
conditionally disable any uniqueness validations (e.g.
|
|
539
540
|
`validates ... unless DemoMode.enabled?`).
|
|
540
541
|
|
|
542
|
+
### Database-backed sequences
|
|
543
|
+
|
|
544
|
+
By default, `CleverSequence` (used by the FactoryBot `sequence` extension) uses an in-memory Ruby counter. For production demo environments running multiple processes or requiring persistence across restarts, you can enable PostgreSQL-backed sequences:
|
|
545
|
+
|
|
546
|
+
```ruby
|
|
547
|
+
DemoMode.configure do
|
|
548
|
+
CleverSequence.use_database_sequences = true
|
|
549
|
+
end
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
This feature flag controls whether `CleverSequence` uses PostgreSQL native sequences or the existing Ruby-based counter, allowing for gradual rollout and easy rollback. By default, `use_database_sequences` is `false`.
|
|
553
|
+
|
|
554
|
+
You can check the current setting with:
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
CleverSequence.use_database_sequences? # => false (default)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
You can also enforce that database sequences exist before they are used. When enabled, `CleverSequence` will raise an error if a sequence is requested but the corresponding PostgreSQL SEQUENCE does not exist yet (prompting the engineer to run a migration that creates the SEQUENCE). When disabled (the default), `CleverSequence` will fall back to calculating the next sequence value based on existing database data:
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
DemoMode.configure do
|
|
564
|
+
CleverSequence.enforce_sequences_exist = true
|
|
565
|
+
end
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
You can check this setting with:
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
CleverSequence.enforce_sequences_exist? # => false (default)
|
|
572
|
+
```
|
|
573
|
+
|
|
541
574
|
## Deploying a demo environment to the cloud
|
|
542
575
|
|
|
543
576
|
This gem truly shines when used to deploy a "demo" version of
|
|
@@ -652,6 +685,13 @@ you operate an "on call" process, engineers should be made aware
|
|
|
652
685
|
that this demo environment _is_ a "production-like" environment
|
|
653
686
|
and should expect "production-like" uptime guarantees.
|
|
654
687
|
|
|
688
|
+
We also emit an `ActiveSupport::Notifications` event
|
|
689
|
+
(`demo_mode.persona_generated`) every time a persona is generated, which can be
|
|
690
|
+
useful for tracking usage over time and alerting to any unexpected spikes,
|
|
691
|
+
drops in usage, and changes to performance. The custom event payload includes the
|
|
692
|
+
persona name and variant, in addition to standard attributes like execution
|
|
693
|
+
duration and exception details (if an error occurred during generation).
|
|
694
|
+
|
|
655
695
|
Again, to learn more about how we use and operate our "demo"
|
|
656
696
|
environments at **Betterment**, check out our ✨ [RailsConf 2022 talk entitled
|
|
657
697
|
"RAILS_ENV=demo"](https://youtu.be/VibJu9IMohc)
|
|
@@ -669,7 +709,7 @@ creating a new issue to get early feedback on your proposed change.
|
|
|
669
709
|
|
|
670
710
|
### Suggested Workflow
|
|
671
711
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
712
|
+
- Fork the project and create a new branch for your contribution.
|
|
713
|
+
- Write your contribution (and any applicable test coverage).
|
|
714
|
+
- Make sure all tests pass (`bundle exec rake`).
|
|
715
|
+
- Submit a pull request.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CleverSequence
|
|
4
|
+
LowerBoundFinder = Struct.new(:klass, :column_name, :block) do
|
|
5
|
+
def lower_bound(current = 1, lower = 0, upper = Float::INFINITY)
|
|
6
|
+
if exists?(current)
|
|
7
|
+
lower_bound(next_between(current, upper), [current, lower].max, upper)
|
|
8
|
+
elsif current - lower > 1
|
|
9
|
+
lower_bound(next_between(lower, current), lower, [current, upper].min)
|
|
10
|
+
else # current should == lower + 1
|
|
11
|
+
lower
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def next_between(lower, upper)
|
|
18
|
+
[((lower + 1) / 2) + (upper / 2), lower * 2].min
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def exists?(value)
|
|
22
|
+
klass.public_send(finder_method, block.call(value))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# TODO: Move onto modern finder methods.
|
|
26
|
+
def finder_method
|
|
27
|
+
:"find_by_#{column_name.to_s.underscore.sub('_crypt', '')}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CleverSequence
|
|
4
|
+
module PostgresBackend
|
|
5
|
+
SEQUENCE_PREFIX = 'cs_'
|
|
6
|
+
|
|
7
|
+
class SequenceNotFoundError < StandardError
|
|
8
|
+
attr_reader :sequence_name, :klass, :attribute
|
|
9
|
+
|
|
10
|
+
def initialize(sequence_name:, klass:, attribute:)
|
|
11
|
+
@sequence_name = sequence_name
|
|
12
|
+
@klass = klass
|
|
13
|
+
@attribute = attribute
|
|
14
|
+
|
|
15
|
+
super(
|
|
16
|
+
"Sequence '#{sequence_name}' not found for #{klass.name}##{attribute}. "
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module SequenceResult
|
|
22
|
+
Exists = Data.define(:sequence_name)
|
|
23
|
+
Missing = Data.define(:sequence_name, :klass, :attribute, :calculated_start_value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def nextval(klass, attribute, block)
|
|
28
|
+
name = sequence_name(klass, attribute)
|
|
29
|
+
|
|
30
|
+
if sequence_exists?(name)
|
|
31
|
+
sequence_cache[name] = SequenceResult::Exists.new(name)
|
|
32
|
+
|
|
33
|
+
result = ActiveRecord::Base.connection.execute(
|
|
34
|
+
"SELECT nextval('#{name}')",
|
|
35
|
+
)
|
|
36
|
+
result.first['nextval'].to_i
|
|
37
|
+
else
|
|
38
|
+
start_value = calculate_sequence_value(klass, attribute, block)
|
|
39
|
+
|
|
40
|
+
sequence_cache[name] = SequenceResult::Missing.new(
|
|
41
|
+
sequence_name: name,
|
|
42
|
+
klass: klass,
|
|
43
|
+
attribute: attribute,
|
|
44
|
+
calculated_start_value: start_value + 1,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if CleverSequence.enforce_sequences_exist
|
|
48
|
+
raise SequenceNotFoundError.new(
|
|
49
|
+
sequence_name: name,
|
|
50
|
+
klass: klass,
|
|
51
|
+
attribute: attribute,
|
|
52
|
+
)
|
|
53
|
+
else
|
|
54
|
+
start_value + 1
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sequence_name(klass, attribute)
|
|
60
|
+
table = klass.table_name.gsub(/[^a-z0-9_]/i, '_')
|
|
61
|
+
attr = attribute.to_s.gsub(/[^a-z0-9_]/i, '_')
|
|
62
|
+
# Handle PostgreSQL identifier limit:
|
|
63
|
+
limit = (63 - SEQUENCE_PREFIX.length) / 2
|
|
64
|
+
"#{SEQUENCE_PREFIX}#{table[0, limit]}_#{attr[0, limit]}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def sequence_cache
|
|
68
|
+
@sequence_cache ||= {}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def sequence_exists?(sequence_name)
|
|
74
|
+
if sequence_cache.key?(sequence_name)
|
|
75
|
+
case sequence_cache[sequence_name]
|
|
76
|
+
when SequenceResult::Exists
|
|
77
|
+
return true
|
|
78
|
+
else
|
|
79
|
+
return false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
ActiveRecord::Base.connection.execute(
|
|
84
|
+
"SELECT 1 FROM information_schema.sequences WHERE sequence_name = '#{sequence_name}' LIMIT 1",
|
|
85
|
+
).any?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def calculate_sequence_value(klass, attribute, block)
|
|
89
|
+
column_name = klass.attribute_aliases.fetch(attribute.to_s, attribute.to_s)
|
|
90
|
+
return 0 unless klass.column_names.include?(column_name)
|
|
91
|
+
|
|
92
|
+
ActiveRecord::Base.with_transactional_lock("lower-bound-#{klass}-#{column_name}") do
|
|
93
|
+
LowerBoundFinder.new(klass, column_name, block).lower_bound
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'clever_sequence/lower_bound_finder'
|
|
4
|
+
require_relative 'clever_sequence/postgres_backend'
|
|
5
|
+
|
|
3
6
|
class CleverSequence
|
|
4
7
|
DEFAULT_BLOCK = ->(i) { i }
|
|
5
8
|
|
|
9
|
+
cattr_accessor(:sequences) { {} }
|
|
10
|
+
cattr_accessor(:use_database_sequences) { false }
|
|
11
|
+
cattr_accessor(:enforce_sequences_exist) { false }
|
|
12
|
+
|
|
6
13
|
class << self
|
|
14
|
+
alias use_database_sequences? use_database_sequences
|
|
15
|
+
alias enforce_sequences_exist? enforce_sequences_exist
|
|
16
|
+
|
|
7
17
|
def reset!
|
|
8
18
|
sequences.each_value(&:reset!)
|
|
9
19
|
end
|
|
@@ -23,7 +33,6 @@ class CleverSequence
|
|
|
23
33
|
end
|
|
24
34
|
end
|
|
25
35
|
|
|
26
|
-
cattr_accessor(:sequences) { {} }
|
|
27
36
|
attr_reader :klass, :attribute, :block
|
|
28
37
|
|
|
29
38
|
def initialize(attribute, &block)
|
|
@@ -38,7 +47,11 @@ class CleverSequence
|
|
|
38
47
|
end
|
|
39
48
|
|
|
40
49
|
def next
|
|
41
|
-
@last_value =
|
|
50
|
+
@last_value = if CleverSequence.use_database_sequences?
|
|
51
|
+
PostgresBackend.nextval(klass, attribute, block)
|
|
52
|
+
else
|
|
53
|
+
last_value + 1
|
|
54
|
+
end
|
|
42
55
|
last
|
|
43
56
|
end
|
|
44
57
|
|
|
@@ -71,31 +84,4 @@ class CleverSequence
|
|
|
71
84
|
def column_exists?
|
|
72
85
|
klass && klass.column_names.include?(column_name)
|
|
73
86
|
end
|
|
74
|
-
|
|
75
|
-
LowerBoundFinder = Struct.new(:klass, :column_name, :block) do
|
|
76
|
-
def lower_bound(current = 1, lower = 0, upper = Float::INFINITY)
|
|
77
|
-
if exists?(current)
|
|
78
|
-
lower_bound(next_between(current, upper), [current, lower].max, upper)
|
|
79
|
-
elsif current - lower > 1
|
|
80
|
-
lower_bound(next_between(lower, current), lower, [current, upper].min)
|
|
81
|
-
else # current should == lower + 1
|
|
82
|
-
lower
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
def next_between(lower, upper)
|
|
89
|
-
[((lower + 1) / 2) + (upper / 2), lower * 2].min
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def exists?(value)
|
|
93
|
-
klass.public_send(finder_method, block.call(value))
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# TODO: Move onto modern finder methods.
|
|
97
|
-
def finder_method
|
|
98
|
-
:"find_by_#{column_name.to_s.underscore.sub('_crypt', '')}"
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
87
|
end
|
data/lib/demo_mode/persona.rb
CHANGED
|
@@ -63,12 +63,14 @@ module DemoMode
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def generate!(variant: :default, password: nil, options: {})
|
|
66
|
-
variant
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
ActiveSupport::Notifications.instrument('demo_mode.persona.generate', name: name, variant: variant) do
|
|
67
|
+
variant = variants[variant]
|
|
68
|
+
CleverSequence.reset! if defined?(CleverSequence)
|
|
69
|
+
DemoMode.current_password = password if password
|
|
70
|
+
DemoMode.around_persona_generation.call(variant.signinable_generator, **options)
|
|
71
|
+
ensure
|
|
72
|
+
DemoMode.current_password = nil
|
|
73
|
+
end
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
def callout(callout = true) # rubocop:disable Style/OptionalBooleanParameter
|
data/lib/demo_mode/version.rb
CHANGED
|
@@ -34,6 +34,14 @@ DemoMode.configure do
|
|
|
34
34
|
# ====================================
|
|
35
35
|
# personas_path 'config/personas'
|
|
36
36
|
|
|
37
|
+
# CleverSequence configuration (for FactoryBot sequence extension):
|
|
38
|
+
# ==================================================================
|
|
39
|
+
# Enable PostgreSQL-backed sequences (defaults to false):
|
|
40
|
+
# CleverSequence.use_database_sequences = true
|
|
41
|
+
#
|
|
42
|
+
# Raise an error if a database sequence doesn't exist (defaults to false):
|
|
43
|
+
# CleverSequence.enforce_sequences_exist = true
|
|
44
|
+
|
|
37
45
|
# A callback that wraps persona-based account generation.
|
|
38
46
|
# You must run `generator.call` and return the "signinable" object:
|
|
39
47
|
# ==================================================
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: demo_mode
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Griffith
|
|
@@ -227,6 +227,20 @@ dependencies:
|
|
|
227
227
|
- - ">="
|
|
228
228
|
- !ruby/object:Gem::Version
|
|
229
229
|
version: '0'
|
|
230
|
+
- !ruby/object:Gem::Dependency
|
|
231
|
+
name: pg
|
|
232
|
+
requirement: !ruby/object:Gem::Requirement
|
|
233
|
+
requirements:
|
|
234
|
+
- - ">="
|
|
235
|
+
- !ruby/object:Gem::Version
|
|
236
|
+
version: '0'
|
|
237
|
+
type: :development
|
|
238
|
+
prerelease: false
|
|
239
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
240
|
+
requirements:
|
|
241
|
+
- - ">="
|
|
242
|
+
- !ruby/object:Gem::Version
|
|
243
|
+
version: '0'
|
|
230
244
|
- !ruby/object:Gem::Dependency
|
|
231
245
|
name: rspec-rails
|
|
232
246
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -324,6 +338,8 @@ files:
|
|
|
324
338
|
- db/migrate/20250210222933_add_demo_mode_sessions_status.rb
|
|
325
339
|
- lib/demo_mode.rb
|
|
326
340
|
- lib/demo_mode/clever_sequence.rb
|
|
341
|
+
- lib/demo_mode/clever_sequence/lower_bound_finder.rb
|
|
342
|
+
- lib/demo_mode/clever_sequence/postgres_backend.rb
|
|
327
343
|
- lib/demo_mode/cli.rb
|
|
328
344
|
- lib/demo_mode/clis/multi_word_search_patch.rb
|
|
329
345
|
- lib/demo_mode/concerns/configurable.rb
|
|
@@ -364,7 +380,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
364
380
|
- !ruby/object:Gem::Version
|
|
365
381
|
version: '0'
|
|
366
382
|
requirements: []
|
|
367
|
-
rubygems_version:
|
|
383
|
+
rubygems_version: 4.0.6
|
|
368
384
|
specification_version: 4
|
|
369
385
|
summary: A configurable demo mode for your Rails app.
|
|
370
386
|
test_files: []
|