Module: Shipeasy

Defined in:
lib/shipeasy/admin.rb,
lib/shipeasy-sdk.rb,
lib/shipeasy/admin.rb,
lib/shipeasy/client.rb,
lib/shipeasy/config.rb,
lib/shipeasy/engine.rb,
lib/shipeasy/logging.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,
lib/generators/shipeasy/install/install_generator.rb

Overview

Leveled logger shared by the whole gem.

Every diagnostic the SDK emits from a caught error goes through here, so a single log_level config option (default :warn) controls the SDK's stderr output. The contract for the SDK's public RUNTIME methods (get_flag, get_config, get_experiment, get_killswitch, track, log_exposure, see, …) is that they NEVER raise into product code — so logging itself is best-effort too: a broken/throwing $stderr can never take down a flag read.

Level ordering (a message at level L is emitted iff the configured level is at least as verbose as L):

silent < error < warn < info < debug

:warn (the default) therefore emits error + warn and suppresses the informational info / debug chatter.

Defined Under Namespace

Modules: Admin, Generators, I18n, Logging, 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.



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

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.



121
122
123
124
125
126
127
128
129
130
# File 'lib/shipeasy/config.rb', line 121

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



268
269
270
271
272
# File 'lib/shipeasy/config.rb', line 268

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.



249
250
251
252
# File 'lib/shipeasy/config.rb', line 249

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

.configObject



94
95
96
# File 'lib/shipeasy/config.rb', line 94

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:



113
114
115
116
117
# File 'lib/shipeasy/config.rb', line 113

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.



212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/shipeasy/config.rb', line 212

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)


198
199
200
201
202
# File 'lib/shipeasy/config.rb', line 198

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



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

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.



134
135
136
137
138
139
140
141
142
143
# File 'lib/shipeasy/config.rb', line 134

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.



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/shipeasy/config.rb', line 350

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.



264
265
266
# File 'lib/shipeasy/config.rb', line 264

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.



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

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.



257
258
259
# File 'lib/shipeasy/config.rb', line 257

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

.override_config(name, value) ⇒ Object



236
237
238
239
# File 'lib/shipeasy/config.rb', line 236

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

.override_experiment(name, group, params) ⇒ Object



241
242
243
244
# File 'lib/shipeasy/config.rb', line 241

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.



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

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.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/shipeasy/config.rb', line 148

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,
    log_level:          cfg.log_level,
  )
  @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
      Shipeasy::Logging.error "[shipeasy] configure(poll) background poll failed: #{e.message}"
    end
  elsif cfg.init
    Thread.new do
      engine.init_once
    rescue => e
      Shipeasy::Logging.error "[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:



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

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.



321
322
323
324
325
326
327
328
329
# File 'lib/shipeasy/config.rb', line 321

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.



277
278
279
# File 'lib/shipeasy/config.rb', line 277

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

.see_violation(name) ⇒ Object



281
282
283
# File 'lib/shipeasy/config.rb', line 281

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