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/internal_report.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, universe(...).assign, get_killswitch, track, 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.



308
309
310
311
312
313
314
315
# File 'lib/shipeasy/config.rb', line 308

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.



128
129
130
131
132
133
134
135
136
137
# File 'lib/shipeasy/config.rb', line 128

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



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

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.



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

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

.configObject



101
102
103
# File 'lib/shipeasy/config.rb', line 101

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:



120
121
122
123
124
# File 'lib/shipeasy/config.rb', line 120

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.



220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/shipeasy/config.rb', line 220

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)


206
207
208
209
210
# File 'lib/shipeasy/config.rb', line 206

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



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

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.



141
142
143
144
145
146
147
148
149
150
# File 'lib/shipeasy/config.rb', line 141

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.



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/shipeasy/config.rb', line 358

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.



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

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.



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

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.



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

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

.override_config(name, value) ⇒ Object



244
245
246
247
# File 'lib/shipeasy/config.rb', line 244

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

.override_experiment(name, group, params) ⇒ Object



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

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.



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

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.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/shipeasy/config.rb', line 155

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,
    disable_internal_error_reporting: cfg.disable_internal_error_reporting,
  )
  @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:



319
320
321
322
323
324
325
326
# File 'lib/shipeasy/config.rb', line 319

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.



329
330
331
332
333
334
335
336
337
# File 'lib/shipeasy/config.rb', line 329

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.



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

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

.see_violation(name) ⇒ Object



289
290
291
# File 'lib/shipeasy/config.rb', line 289

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