Module: Shipeasy

Defined in:
lib/shipeasy/config.rb,
lib/shipeasy-sdk.rb,
lib/shipeasy/client.rb,
lib/shipeasy/engine.rb,
lib/shipeasy/sdk/see.rb,
lib/shipeasy/sdk/eval.rb,
lib/shipeasy/sdk/skill.rb,
lib/shipeasy/sdk/anon_id.rb,
lib/shipeasy/sdk/murmur3.rb,
lib/shipeasy/sdk/railtie.rb,
lib/shipeasy/sdk/version.rb,
lib/shipeasy/i18n/railtie.rb,
lib/shipeasy/sdk/telemetry.rb,
lib/shipeasy/sdk/openfeature.rb,
lib/shipeasy/sdk/sticky_store.rb,
lib/shipeasy/i18n/view_helpers.rb,
lib/shipeasy/i18n/label_fetcher.rb,
lib/shipeasy/sdk/rack_middleware.rb

Overview

Single configuration object for the Shipeasy gem.

Covers both subsystems:

- SDK / experimentation (api_key, base_url) — drives Engine
- i18n / string manager (public_key, profile, cdn_base_url, ...) — drives
  the Rails view helpers and label fetcher

Usage:

Shipeasy.configure do |c|
  c.api_key    = ENV["SHIPEASY_SERVER_KEY"]
  c.public_key = ENV["SHIPEASY_CLIENT_KEY"]
  c.profile    = "default"
end

Anything not set falls back to the defaults below. The same Shipeasy.config is read by Engine and the Rails helpers, so there is one place to point environment variables at.

Defined Under Namespace

Modules: I18n, OpenFeature, SDK Classes: Client, Configuration, Engine, Error

Class Method Summary collapse

Class Method Details

.apply_overrides(engine, flags, configs, experiments) ⇒ Object

Apply the configure_for_* override args onto an engine.



290
291
292
293
294
295
296
297
# File 'lib/shipeasy/config.rb', line 290

def apply_overrides(engine, flags, configs, experiments)
  (flags || {}).each { |name, value| engine.override_flag(name, value) }
  (configs || {}).each { |name, value| engine.override_config(name, value) }
  (experiments || {}).each do |name, spec|
    group, params = spec   # spec is [group, params]
    engine.override_experiment(name, group, params)
  end
end

.attributes_transformObject

The resolved attributes transform (callable). Default = identity, so a user object that is already the attribute hash is used verbatim.



112
113
114
115
116
117
118
119
120
121
# File 'lib/shipeasy/config.rb', line 112

def attributes_transform
  transform = config.attributes
  if transform.nil?
    ->(user) { user }
  elsif transform.respond_to?(:call)
    transform
  else
    raise Error, "Shipeasy.configure { |c| c.attributes = … } must be a callable (e.g. a lambda)"
  end
end

.bootstrap_script_tag(user, anon_id: nil, i18n_profile: "en:prod", base_url: nil) ⇒ Object



258
259
260
261
262
# File 'lib/shipeasy/config.rb', line 258

def bootstrap_script_tag(user, anon_id: nil, i18n_profile: "en:prod", base_url: nil)
  require_engine("bootstrap_script_tag").bootstrap_script_tag(
    user, anon_id: anon_id, i18n_profile: i18n_profile, base_url: base_url
  )
end

.clear_overridesObject

Drop EVERY override — including the seed from configure_for_testing (test mode has no blob beneath); under configure_for_offline it reverts to the snapshot.



239
240
241
242
# File 'lib/shipeasy/config.rb', line 239

def clear_overrides
  require_engine("clear_overrides").clear_overrides
  nil
end

.configObject



85
86
87
# File 'lib/shipeasy/config.rb', line 85

def config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Configure the gem once at boot. In addition to populating the shared Configuration, this builds and registers the ONE global Shipeasy::Engine (first-config-wins) from the api_key/base_url and kicks off its one-shot fetch (fire-and-forget) so ‘Shipeasy::Client.new(user).get_flag(…)` resolves against real rules with no explicit init call.

Shipeasy.configure do |c|
  c.api_key    = ENV["SHIPEASY_SERVER_KEY"]
  c.attributes = ->(u) { { "user_id" => u.id, "plan" => u.plan } }
end

Shipeasy::Client.new(current_user).get_flag("new_checkout")

Long-running servers that also want the background poll can call ‘Shipeasy.engine.init` after configure.

Yields:



104
105
106
107
108
# File 'lib/shipeasy/config.rb', line 104

def configure
  yield config
  register_engine!(config) if config.api_key
  config
end

.configure_for_offline(snapshot: nil, path: nil, flags: nil, configs: nil, experiments: nil, attributes: nil) ⇒ Object

Configure Shipeasy OFFLINE — evaluate the REAL rules from an in-memory snapshot or a JSON file, with no network. Provide exactly one source:

snapshot: { "flags" => <body of /sdk/flags>, "experiments" => <body of /sdk/experiments> }
path:     "snapshot.json"   (a JSON file of the same shape)

Optional flags/configs/experiments overrides layer on top (same shapes as configure_for_testing). Replaces any previously-configured engine.



202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/shipeasy/config.rb', line 202

def configure_for_offline(snapshot: nil, path: nil, flags: nil, configs: nil, experiments: nil, attributes: nil)
  engine =
    if path
      Engine.from_file(path)
    elsif snapshot
      s = snapshot.transform_keys(&:to_s)
      Engine.from_snapshot(flags: s["flags"], experiments: s["experiments"])
    else
      raise Error, "Shipeasy.configure_for_offline requires snapshot: or path:"
    end
  apply_overrides(engine, flags, configs, experiments)
  install_global_engine(engine, attributes)
end

.configure_for_testing(flags: nil, configs: nil, experiments: nil, attributes: nil) ⇒ Object

Configure Shipeasy in TEST MODE — no api key, zero network, ever. Seed the values your code under test should see via the override args, then read through the ordinary ‘Shipeasy::Client.new(user)`:

Shipeasy.configure_for_testing(flags: { "new_checkout" => true })
Shipeasy::Client.new({ "user_id" => "u_1" }).get_flag("new_checkout") # => true

flags:       { name => bool }              forced get_flag results
configs:     { name => value }             forced get_config results
experiments: { name => [group, params] }   forced enrolments
attributes:  same transform as configure (default identity)


188
189
190
191
192
# File 'lib/shipeasy/config.rb', line 188

def configure_for_testing(flags: nil, configs: nil, experiments: nil, attributes: nil)
  engine = Engine.for_testing
  apply_overrides(engine, flags, configs, experiments)
  install_global_engine(engine, attributes)
end

.control_flow_exception(err) ⇒ Object



275
276
277
# File 'lib/shipeasy/config.rb', line 275

def control_flow_exception(err)
  Shipeasy::SDK.control_flow_exception(err)
end

.engineObject

The single global engine registered by configure, or nil if configure has not run (or ran without an api_key). Shipeasy::Client reads this.



125
126
127
128
129
130
131
132
133
134
# File 'lib/shipeasy/config.rb', line 125

def engine
  pid = Process.pid
  if @engine && @engine_pid != pid
    # Post-fork: the parent's poll thread didn't survive. Rebuild lazily
    # from the stored config in this child process.
    @engine = nil
    register_engine!(config) if config.api_key
  end
  @engine
end

.flagsObject

Lazy, fork-safe singleton Engine. The first call from each process spawns a fresh client + poll thread — including post-fork workers under Puma’s preload_app!. Callers can ‘Shipeasy.flags.get_flag(…)` straight from a controller without holding a constant or worrying about `before_worker_boot` hooks.

Initializers stay minimal:

# config/initializers/shipeasy.rb
Shipeasy.configure { |c| c.api_key = ENV["SHIPEASY_SERVER_KEY"] }

The first request that touches ‘Shipeasy.flags.*` triggers init(). For serverless / Lambda where you want a single fetch with no thread, build the engine explicitly: `Shipeasy::Engine.new(…).init_once`.

NOTE: this remains a separate, polling engine from the one configure() registers (Shipeasy.engine). New code should prefer the Shipeasy.configure + Shipeasy::Client.new(user) front door; ‘Shipeasy.flags` is retained for the legacy `Shipeasy.flags.get_flag(name, user)` style.



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/shipeasy/config.rb', line 340

def flags
  pid = Process.pid
  if @flags && @flags_pid != pid
    # Post-fork: parent's poll thread didn't survive. Don't destroy
    # @flags (its mutex/state is invalid in this child anyway); just
    # rebuild from scratch.
    @flags = nil
  end
  @flags ||= begin
    @flags_pid = pid
    client = Engine.new(
      api_key:  config.api_key,
      base_url: config.base_url,
    )
    client.init
    client
  end
end

.i18n_script_tag(client_key, profile: "en:prod", base_url: nil) ⇒ Object

SSR tag helpers — delegate to the configured global engine, so you never touch it. i18n_script_tag carries the PUBLIC client key (not the server key); bootstrap_script_tag embeds no key.



254
255
256
# File 'lib/shipeasy/config.rb', line 254

def i18n_script_tag(client_key, profile: "en:prod", base_url: nil)
  require_engine("i18n_script_tag").i18n_script_tag(client_key, profile: profile, base_url: base_url)
end

.install_global_engine(engine, attributes) ⇒ Object

Replace the registered global engine + attributes transform (used by the configure_for_* siblings — unlike configure, they replace so a test suite can reconfigure between cases). Returns the engine.



282
283
284
285
286
287
# File 'lib/shipeasy/config.rb', line 282

def install_global_engine(engine, attributes)
  config.attributes = attributes
  @engine = engine
  @engine_pid = Process.pid
  engine
end

.on_change(callable = nil, &block) ⇒ Object

Register a poll listener fired after a background poll fetches NEW data (HTTP 200, not 304). Requires configure(poll: true). Returns an unsubscribe proc. Accepts a block or any callable.



247
248
249
# File 'lib/shipeasy/config.rb', line 247

def on_change(callable = nil, &block)
  require_engine("on_change").on_change(callable, &block)
end

.override_config(name, value) ⇒ Object



226
227
228
229
# File 'lib/shipeasy/config.rb', line 226

def override_config(name, value)
  require_engine("override_config").override_config(name, value)
  nil
end

.override_experiment(name, group, params) ⇒ Object



231
232
233
234
# File 'lib/shipeasy/config.rb', line 231

def override_experiment(name, group, params)
  require_engine("override_experiment").override_experiment(name, group, params)
  nil
end

.override_flag(name, value) ⇒ Object

On-the-spot overrides layered on top of whatever configure_for_testing / configure_for_offline (or a live configure) set up — they win over the blob until clear_overrides. Require a prior configure* call.



221
222
223
224
# File 'lib/shipeasy/config.rb', line 221

def override_flag(name, value)
  require_engine("override_flag").override_flag(name, value)
  nil
end

.register_engine!(cfg) ⇒ Object

Build + register the one global engine (first-config-wins). Kicks off the configured fetch lifecycle (one-shot by default; the background poll when ‘c.poll = true`) fire-and-forget. Idempotent within a process.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/shipeasy/config.rb', line 139

def register_engine!(cfg)
  return @engine if @engine && @engine_pid == Process.pid
  @engine_pid = Process.pid
  engine = Engine.new(
    api_key:            cfg.api_key,
    base_url:           cfg.base_url,
    env:                cfg.env,
    disable_telemetry:  cfg.disable_telemetry,
    telemetry_url:      cfg.telemetry_url,
    private_attributes: cfg.private_attributes,
    sticky_store:       cfg.sticky_store,
  )
  @engine = engine
  # Capture +engine+ in the closure (not the @engine ivar, which a concurrent
  # reset/reconfigure could nil out before the thread runs).
  if cfg.poll
    Thread.new do
      engine.init   # initial fetch + background poll thread
    rescue => e
      warn "[shipeasy] configure(poll) background poll failed: #{e.message}"
    end
  elsif cfg.init
    Thread.new do
      engine.init_once
    rescue => e
      warn "[shipeasy] configure() one-shot fetch failed: #{e.message}"
    end
  end
  engine
end

.require_engine(fn_name) ⇒ Object

The global engine, or raise a helpful error naming the package-level fn the caller used before any configure*.

Raises:



301
302
303
304
305
306
307
308
# File 'lib/shipeasy/config.rb', line 301

def require_engine(fn_name)
  e = engine
  return e unless e.nil?

  raise Error, "Shipeasy.#{fn_name} called before Shipeasy.configure " \
               "{ |c| c.api_key = … } (or configure_for_testing / " \
               "configure_for_offline). Call one once at app boot."
end

.reset_config!Object

Reset the config back to defaults — primarily for tests.



311
312
313
314
315
316
317
318
319
# File 'lib/shipeasy/config.rb', line 311

def reset_config!
  @config = nil
  @flags_pid = nil
  @flags&.destroy
  @flags = nil
  @engine&.destroy
  @engine = nil
  @engine_pid = nil
end

.see(problem) ⇒ Object

see() structured error reporting — package-level, dispatched through the last-constructed default client (the engine configure built). Never raises into caller code; a call before any client exists warns and no-ops.



267
268
269
# File 'lib/shipeasy/config.rb', line 267

def see(problem)
  Shipeasy::SDK.see(problem)
end

.see_violation(name) ⇒ Object



271
272
273
# File 'lib/shipeasy/config.rb', line 271

def see_violation(name)
  Shipeasy::SDK.see_violation(name)
end