shipeasy-sdk 1.7.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afdce684e3c6a83b220b7dfaa1d7db249284e3bb4c4ebfe3c9b27c54da0d801b
4
- data.tar.gz: ecdfdf8aed19f1b5f18fe874ce5b13064bb61abe7bf7f1c47a76b08d9ab99bce
3
+ metadata.gz: 97efde389fa7a5adc1ffdab3951d09837f435bd529d8f6c3b7e3d45387472628
4
+ data.tar.gz: 46f183776d94ccbddd8781e39d3ea5e56e6795cf26f3292f1482c2886b7d0836
5
5
  SHA512:
6
- metadata.gz: e6a5470268bc584eaa2dbc8c89f722a22633d0bd6f896aaa1d73db74090b56667c87733c614b8a49bdeda88dab043740509153503943837a69be20a03dc315b7
7
- data.tar.gz: 55289a4adf0559f41eca2d1f6bca03b53a19da9b3f00f4109dc4476ed0c5cbaaed453dbaf3f7c0ebcc1405f0d73a55fe702e02859c9a809d525e92b294e275ba
6
+ metadata.gz: 010654d1322131ab21b467fdff8176914e427558a5a620762c4417378f68c5cc5f3b189e7b0cdb8de6b741d2232e7c1552b8fd551bc3fce19b09e91263e18d12
7
+ data.tar.gz: 24730826e8f2de0ac3f1546ab4f42b2301bcd8f83bdaea0b9c50487d7b90d5b445470827199de73c4794bc16b2ddeed3e18fc44079c2000cd0c92aeb07f50e55
data/README.md CHANGED
@@ -14,34 +14,56 @@ gem "shipeasy-sdk"
14
14
 
15
15
  ## Quickstart (Rails)
16
16
 
17
- `config/initializers/shipeasy.rb` is all you need:
17
+ Two parts: **configure once** at boot, then build a **user-bound
18
+ `Shipeasy::Client`** per request via its constructor.
19
+
20
+ `config/initializers/shipeasy.rb`:
18
21
 
19
22
  ```ruby
20
23
  Shipeasy.configure do |c|
21
24
  c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY")
25
+
26
+ # Optional: map YOUR user object → the Shipeasy attribute hash. Runs once,
27
+ # in the Shipeasy::Client constructor. Omit it and the object you pass to
28
+ # Shipeasy::Client.new IS the attribute hash (identity).
29
+ c.attributes = ->(u) { { "user_id" => u.id, "plan" => u.plan } }
30
+
22
31
  c.public_key = ENV.fetch("SHIPEASY_CLIENT_KEY") # for i18n view helpers
23
32
  c.profile = "default"
24
33
  end
25
34
  ```
26
35
 
27
- Anywhere in your app:
36
+ `configure` builds the single global engine for you and kicks off a one-shot
37
+ fetch (fire-and-forget). Anywhere in your app, construct a bound client and call
38
+ the getters with **no user argument** — the user is bound at construction:
28
39
 
29
40
  ```ruby
30
- user = { user_id: current_user.id, plan: current_user.plan }
41
+ flags = Shipeasy::Client.new(current_user) # runs the attributes transform once
31
42
 
32
- if Shipeasy.flags.get_flag("new_checkout", user)
43
+ if flags.get_flag("new_checkout") # NO user arg
33
44
  # ship it
34
45
  end
35
46
 
36
- color = Shipeasy.flags.get_config("button_color")
37
- result = Shipeasy.flags.get_experiment("checkout_cta", user, { label: "Buy now" })
38
- Shipeasy.flags.track(current_user.id.to_s, "checkout_completed", { revenue: 49.99 })
47
+ color = flags.get_config("button_color")
48
+ result = flags.get_experiment("checkout_cta", { label: "Buy now" })
49
+ panic = flags.get_killswitch("payments")
39
50
  ```
40
51
 
41
- `Shipeasy.flags` is a lazy, **fork-safe** singleton: the first call from
42
- each process spawns its own `FlagsClient` and starts the background poll
43
- thread, including post-fork Puma workers under `preload_app!`. No need
44
- for `before_worker_boot` hooks or holding a global constant.
52
+ `Shipeasy::Client` is **cheap**: it delegates evaluation to the single engine
53
+ built by `configure` — it never opens its own connection, fetches, or polls.
54
+ Construct one per user / per request.
55
+
56
+ Event ingestion (`track`) lives on the engine — `Shipeasy.engine` is the global
57
+ one `configure` registered:
58
+
59
+ ```ruby
60
+ Shipeasy.engine.track(current_user.id.to_s, "checkout_completed", { revenue: 49.99 })
61
+ ```
62
+
63
+ > **Upgrading from 1.x?** The heavyweight client was renamed
64
+ > `Shipeasy::SDK::FlagsClient` → `Shipeasy::Engine`, and `Shipeasy::Client` is
65
+ > now the lightweight user-bound handle. The legacy `Shipeasy.flags.get_flag(name, user)`
66
+ > singleton still works.
45
67
 
46
68
  In a Rails view (the railtie auto-mounts these helpers when Rails is loaded):
47
69
 
@@ -61,7 +83,7 @@ wiring** — `get_flag` on an anonymous request just works:
61
83
 
62
84
  ```ruby
63
85
  # current_user is nil → buckets on the __se_anon_id cookie automatically
64
- Shipeasy.flags.get_flag("new_checkout", {})
86
+ Shipeasy::Client.new({}).get_flag("new_checkout")
65
87
  ```
66
88
 
67
89
  An explicit `user_id` / `anonymous_id` always wins. If you prefer to read the id
@@ -86,7 +108,8 @@ require "shipeasy-sdk"
86
108
 
87
109
  Shipeasy.configure { |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY") }
88
110
 
89
- Shipeasy.flags.get_flag("new_checkout", { user_id: "u_1" })
111
+ # With no `attributes` transform, the hash you pass IS the attribute map.
112
+ Shipeasy::Client.new({ "user_id" => "u_1" }).get_flag("new_checkout")
90
113
  ```
91
114
 
92
115
  The Rails view helpers (`i18n_*`) are not loaded outside Rails, so the
@@ -99,9 +122,9 @@ short-lived function. Build the client explicitly and call `init_once`
99
122
  for a single synchronous fetch:
100
123
 
101
124
  ```ruby
102
- client = Shipeasy::SDK::FlagsClient.new(api_key: ENV.fetch("SHIPEASY_SERVER_KEY"))
103
- client.init_once
104
- client.get_flag("new_checkout", user)
125
+ engine = Shipeasy::Engine.new(api_key: ENV.fetch("SHIPEASY_SERVER_KEY"))
126
+ engine.init_once
127
+ engine.get_flag("new_checkout", user)
105
128
  ```
106
129
 
107
130
  ## Lifecycle escape hatch
@@ -206,9 +229,9 @@ endpoints:
206
229
  ```
207
230
 
208
231
  ```ruby
209
- client = Shipeasy::SDK::FlagsClient.from_file("snapshot.json")
232
+ client = Shipeasy::Engine.from_file("snapshot.json")
210
233
  # or, from already-parsed blobs:
211
- client = Shipeasy::SDK::FlagsClient.from_snapshot(flags: flags_body, experiments: exps_body)
234
+ client = Shipeasy::Engine.from_snapshot(flags: flags_body, experiments: exps_body)
212
235
 
213
236
  client.get_flag("new_checkout", user) # real evaluation, no network
214
237
  client.get_experiment("checkout_cta", user, {})
@@ -235,7 +258,7 @@ snapshot.
235
258
 
236
259
  For unit/integration tests you want a client that does **zero network** and
237
260
  returns exactly the values you seed — no api_key, no fetch, no poll thread, no
238
- telemetry, no metric ingestion. Build one with `FlagsClient.for_testing` and
261
+ telemetry, no metric ingestion. Build one with `Shipeasy::Engine.for_testing` and
239
262
  seed each entity with the `override_*` setters (Statsig-style local overrides).
240
263
  An override always wins over the fetched blob, so the getters answer
241
264
  deterministically:
@@ -243,7 +266,7 @@ deterministically:
243
266
  ```ruby
244
267
  require "shipeasy-sdk"
245
268
 
246
- client = Shipeasy::SDK::FlagsClient.for_testing
269
+ client = Shipeasy::Engine.for_testing
247
270
  # init / init_once are no-ops here — nothing is ever fetched.
248
271
 
249
272
  # Flags (boolean)
@@ -272,31 +295,35 @@ client.clear_overrides
272
295
 
273
296
  The same `override_flag` / `override_config` / `override_experiment` /
274
297
  `clear_overrides` setters also work on a **normal** live client (built with
275
- `FlagsClient.new(...)`), so you can pin one value in local development while the
298
+ `Shipeasy::Engine.new(...)`), so you can pin one value in local development while the
276
299
  rest comes from the fetched blob.
277
300
 
278
- ### Rails singleton
301
+ ### Global engine / bound client
279
302
 
280
- `Shipeasy.flags` is a process-wide singleton that fetches over the network, so
281
- in tests prefer a `for_testing` client. If a code path reaches through
282
- `Shipeasy.flags` directly, stub the singleton to the test client in your test
283
- setup:
303
+ `Shipeasy.engine` (registered by `configure`) and `Shipeasy.flags` (legacy
304
+ singleton) both fetch over the network, so in tests stub them to a
305
+ `for_testing` engine. `Shipeasy::Client.new(user)` reads `Shipeasy.engine`, so
306
+ stubbing the engine also covers the bound-client path:
284
307
 
285
308
  ```ruby
286
309
  # RSpec
287
310
  before do
288
- test_client = Shipeasy::SDK::FlagsClient.for_testing
289
- test_client.override_flag("new_checkout", true)
290
- allow(Shipeasy).to receive(:flags).and_return(test_client)
311
+ test_engine = Shipeasy::Engine.for_testing
312
+ test_engine.override_flag("new_checkout", true)
313
+ allow(Shipeasy).to receive(:engine).and_return(test_engine)
314
+ allow(Shipeasy).to receive(:flags).and_return(test_engine) # legacy path
315
+
316
+ # Shipeasy::Client.new(user).get_flag("new_checkout") now => true
291
317
  end
292
318
  ```
293
319
 
294
320
  ## Configuration
295
321
 
296
- | Parameter | Default | Description |
297
- | ---------- | ------------------------- | ----------------------------------- |
298
- | `api_key` | (required) | SDK key from the Shipeasy dashboard |
299
- | `base_url` | `https://cdn.shipeasy.ai` | Override for local dev / staging |
322
+ | Parameter | Default | Description |
323
+ | ------------ | ----------------------------- | ------------------------------------------------------------------- |
324
+ | `api_key` | (required) | SDK key from the Shipeasy dashboard |
325
+ | `base_url` | `https://edge.shipeasy.dev` | Override for local dev / staging |
326
+ | `attributes` | identity (`->(u) { u }`) | Callable mapping your user object → the Shipeasy attribute hash |
300
327
 
301
328
  ## Documentation
302
329
 
@@ -0,0 +1,62 @@
1
+ module Shipeasy
2
+ # A lightweight, user-bound evaluation handle. Construct one per user/request
3
+ # via its real constructor:
4
+ #
5
+ # flags = Shipeasy::Client.new(current_user)
6
+ # flags.get_flag("new_checkout") # NO user arg — bound at construction
7
+ # flags.get_experiment("price_test", { price: 9 })
8
+ #
9
+ # It is cheap: it delegates every evaluation to the single global engine built
10
+ # by `Shipeasy.configure { … }`. It does NOT open its own HTTP connection,
11
+ # fetch, or start a poll timer.
12
+ #
13
+ # The configured `attributes` transform (see Shipeasy::Configuration#attributes)
14
+ # runs ONCE here, in the constructor, against the raw user object you pass.
15
+ # The resulting attribute hash is then enriched with the request-scoped
16
+ # anonymous_id (when you supplied neither user_id nor anonymous_id) and bound,
17
+ # so every getter reads the same bag.
18
+ #
19
+ # Raises if constructed before `Shipeasy.configure` registered an engine.
20
+ class Client
21
+ # The resolved attribute hash this handle evaluates against.
22
+ attr_reader :attributes
23
+
24
+ def initialize(user)
25
+ engine = Shipeasy.engine
26
+ if engine.nil?
27
+ raise Error, "Shipeasy::Client.new(user) called before Shipeasy.configure " \
28
+ "{ |c| c.api_key = … }. Call Shipeasy.configure once at app boot."
29
+ end
30
+ @engine = engine
31
+ # Run the configured attributes transform (default identity), then apply
32
+ # the existing anon-id merge exactly as the per-call engine path does.
33
+ mapped = Shipeasy.attributes_transform.call(user)
34
+ @attributes = engine.bind_attributes(mapped)
35
+ end
36
+
37
+ def get_flag(name, default: false)
38
+ @engine.get_flag(name, @attributes, default: default)
39
+ end
40
+
41
+ def get_flag_detail(name)
42
+ @engine.get_flag_detail(name, @attributes)
43
+ end
44
+
45
+ # Configs are not user-scoped, but exposed here for one-stop ergonomics.
46
+ def get_config(name, decode = nil, default: nil)
47
+ @engine.get_config(name, decode, default: default)
48
+ end
49
+
50
+ def get_experiment(name, default_params, decode = nil)
51
+ @engine.get_experiment(name, @attributes, default_params, decode)
52
+ end
53
+
54
+ # Killswitches are not user-scoped; forwarded straight to the engine.
55
+ def get_killswitch(name, switch_key = nil)
56
+ @engine.get_killswitch(name, switch_key)
57
+ end
58
+ end
59
+
60
+ # Raised by Shipeasy::Client when constructed before Shipeasy.configure.
61
+ class Error < StandardError; end
62
+ end
@@ -1,7 +1,7 @@
1
1
  # Single configuration object for the Shipeasy gem.
2
2
  #
3
3
  # Covers both subsystems:
4
- # - SDK / experimentation (api_key, base_url) — drives FlagsClient
4
+ # - SDK / experimentation (api_key, base_url) — drives Engine
5
5
  # - i18n / string manager (public_key, profile, cdn_base_url, ...) — drives
6
6
  # the Rails view helpers and label fetcher
7
7
  #
@@ -14,7 +14,7 @@
14
14
  # end
15
15
  #
16
16
  # Anything not set falls back to the defaults below. The same Shipeasy.config
17
- # is read by FlagsClient and the Rails helpers, so there is one place to
17
+ # is read by Engine and the Rails helpers, so there is one place to
18
18
  # point environment variables at.
19
19
 
20
20
  module Shipeasy
@@ -22,6 +22,18 @@ module Shipeasy
22
22
  # ---- experimentation / SDK ----
23
23
  attr_accessor :api_key, :base_url
24
24
 
25
+ # Optional transform from YOUR user object (any shape) to the Shipeasy
26
+ # attribute hash every flag/experiment evaluation uses. A callable
27
+ # (lambda/proc or anything responding to #call). Default = identity (the
28
+ # user object is assumed to already BE the attribute hash). Runs once, in
29
+ # the Shipeasy::Client constructor.
30
+ #
31
+ # Shipeasy.configure do |c|
32
+ # c.api_key = ENV["SHIPEASY_SERVER_KEY"]
33
+ # c.attributes = ->(u) { { "user_id" => u.id, "plan" => u.plan } }
34
+ # end
35
+ attr_accessor :attributes
36
+
25
37
  # ---- i18n / string manager ----
26
38
  attr_accessor :public_key, :profile, :default_chunk,
27
39
  :cdn_base_url, :loader_url,
@@ -29,6 +41,7 @@ module Shipeasy
29
41
 
30
42
  def initialize
31
43
  @base_url = "https://edge.shipeasy.dev"
44
+ @attributes = nil
32
45
 
33
46
  @profile = "default"
34
47
  @default_chunk = "index"
@@ -45,8 +58,68 @@ module Shipeasy
45
58
  @config ||= Configuration.new
46
59
  end
47
60
 
61
+ # Configure the gem once at boot. In addition to populating the shared
62
+ # Configuration, this builds and registers the ONE global Shipeasy::Engine
63
+ # (first-config-wins) from the api_key/base_url and kicks off its one-shot
64
+ # fetch (fire-and-forget) so `Shipeasy::Client.new(user).get_flag(...)`
65
+ # resolves against real rules with no explicit init call.
66
+ #
67
+ # Shipeasy.configure do |c|
68
+ # c.api_key = ENV["SHIPEASY_SERVER_KEY"]
69
+ # c.attributes = ->(u) { { "user_id" => u.id, "plan" => u.plan } }
70
+ # end
71
+ #
72
+ # Shipeasy::Client.new(current_user).get_flag("new_checkout")
73
+ #
74
+ # Long-running servers that also want the background poll can call
75
+ # `Shipeasy.engine.init` after configure.
48
76
  def configure
49
77
  yield config
78
+ register_engine!(config) if config.api_key
79
+ config
80
+ end
81
+
82
+ # The resolved attributes transform (callable). Default = identity, so a
83
+ # user object that is already the attribute hash is used verbatim.
84
+ def attributes_transform
85
+ transform = config.attributes
86
+ if transform.nil?
87
+ ->(user) { user }
88
+ elsif transform.respond_to?(:call)
89
+ transform
90
+ else
91
+ raise Error, "Shipeasy.configure { |c| c.attributes = … } must be a callable (e.g. a lambda)"
92
+ end
93
+ end
94
+
95
+ # The single global engine registered by configure, or nil if configure has
96
+ # not run (or ran without an api_key). Shipeasy::Client reads this.
97
+ def engine
98
+ pid = Process.pid
99
+ if @engine && @engine_pid != pid
100
+ # Post-fork: the parent's poll thread didn't survive. Rebuild lazily
101
+ # from the stored config in this child process.
102
+ @engine = nil
103
+ register_engine!(config) if config.api_key
104
+ end
105
+ @engine
106
+ end
107
+
108
+ # Build + register the one global engine (first-config-wins). Fires the
109
+ # one-shot fetch fire-and-forget. Idempotent within a process.
110
+ def register_engine!(cfg)
111
+ return @engine if @engine && @engine_pid == Process.pid
112
+ @engine_pid = Process.pid
113
+ engine = Engine.new(api_key: cfg.api_key, base_url: cfg.base_url)
114
+ @engine = engine
115
+ # Capture +engine+ in the closure (not the @engine ivar, which a concurrent
116
+ # reset/reconfigure could nil out before the thread runs).
117
+ Thread.new do
118
+ engine.init_once
119
+ rescue => e
120
+ warn "[shipeasy] configure() one-shot fetch failed: #{e.message}"
121
+ end
122
+ engine
50
123
  end
51
124
 
52
125
  # Reset the config back to defaults — primarily for tests.
@@ -55,9 +128,12 @@ module Shipeasy
55
128
  @flags_pid = nil
56
129
  @flags&.destroy
57
130
  @flags = nil
131
+ @engine&.destroy
132
+ @engine = nil
133
+ @engine_pid = nil
58
134
  end
59
135
 
60
- # Lazy, fork-safe singleton FlagsClient. The first call from each
136
+ # Lazy, fork-safe singleton Engine. The first call from each
61
137
  # process spawns a fresh client + poll thread — including post-fork
62
138
  # workers under Puma's preload_app!. Callers can `Shipeasy.flags.get_flag(...)`
63
139
  # straight from a controller without holding a constant or worrying
@@ -70,7 +146,12 @@ module Shipeasy
70
146
  #
71
147
  # The first request that touches `Shipeasy.flags.*` triggers init().
72
148
  # For serverless / Lambda where you want a single fetch with no thread,
73
- # build the client explicitly: `Shipeasy::SDK::FlagsClient.new(...).init_once`.
149
+ # build the engine explicitly: `Shipeasy::Engine.new(...).init_once`.
150
+ #
151
+ # NOTE: this remains a separate, polling engine from the one configure()
152
+ # registers (Shipeasy.engine). New code should prefer the
153
+ # Shipeasy.configure + Shipeasy::Client.new(user) front door; `Shipeasy.flags`
154
+ # is retained for the legacy `Shipeasy.flags.get_flag(name, user)` style.
74
155
  def flags
75
156
  pid = Process.pid
76
157
  if @flags && @flags_pid != pid
@@ -81,7 +162,7 @@ module Shipeasy
81
162
  end
82
163
  @flags ||= begin
83
164
  @flags_pid = pid
84
- client = SDK::FlagsClient.new(
165
+ client = Engine.new(
85
166
  api_key: config.api_key,
86
167
  base_url: config.base_url,
87
168
  )
@@ -3,15 +3,31 @@ require "uri"
3
3
  require "json"
4
4
  require "thread"
5
5
  require "cgi"
6
- require_relative "eval"
7
- require_relative "telemetry"
8
- require_relative "anon_id"
9
- require_relative "sticky_store"
10
- require_relative "see"
6
+ require_relative "sdk/eval"
7
+ require_relative "sdk/telemetry"
8
+ require_relative "sdk/anon_id"
9
+ require_relative "sdk/sticky_store"
10
+ require_relative "sdk/see"
11
11
 
12
12
  module Shipeasy
13
- module SDK
14
- class FlagsClient
13
+ # The heavyweight engine: owns the api key, HTTP transport, the blob cache,
14
+ # the background poll timer, init/init_once, local overrides, track, and
15
+ # see()/default-client wiring. Was `Shipeasy::SDK::FlagsClient` before 2.0;
16
+ # renamed to a clean top-level `Shipeasy::Engine` when the lightweight
17
+ # user-bound `Shipeasy::Client` became the primary front door.
18
+ #
19
+ # Most apps never construct an Engine directly — `Shipeasy.configure { … }`
20
+ # builds and registers the one global engine for you. Construct one explicitly
21
+ # only for advanced/serverless flows (multiple keys, offline snapshots).
22
+ class Engine
23
+ # Internal collaborators still live under Shipeasy::SDK; alias them so the
24
+ # body below can keep referring to them unqualified after the class moved
25
+ # out from under the SDK namespace.
26
+ Eval = Shipeasy::SDK::Eval
27
+ Telemetry = Shipeasy::SDK::Telemetry
28
+ AnonId = Shipeasy::SDK::AnonId
29
+ See = Shipeasy::SDK::See
30
+
15
31
  DEFAULT_BASE_URL = "https://edge.shipeasy.dev"
16
32
  # CDN origin serving the static loader scripts (/sdk/bootstrap.js,
17
33
  # /sdk/i18n/loader.js) — distinct from the edge API the blobs are fetched from.
@@ -34,7 +50,7 @@ module Shipeasy
34
50
  @sticky_store = sticky_store
35
51
  # Test mode: no network, ever. init/init_once/track become no-ops and
36
52
  # evaluation answers come purely from local overrides. Built via the
37
- # FlagsClient.for_testing factory; see clear_overrides / override_*.
53
+ # Engine.for_testing factory; see clear_overrides / override_*.
38
54
  @test_mode = test_mode
39
55
  # Per-evaluation usage telemetry. ON by default; pass
40
56
  # disable_telemetry: true to opt out. See telemetry.rb.
@@ -275,6 +291,28 @@ module Shipeasy
275
291
  result
276
292
  end
277
293
 
294
+ # Public hook for the bound Shipeasy::Client: normalise an attribute hash
295
+ # and apply the request-scoped anonymous_id merge ONCE, at Client
296
+ # construction, exactly as every per-call getter does internally.
297
+ def bind_attributes(user)
298
+ with_anon_id(user)
299
+ end
300
+
301
+ # Read a killswitch from the cached flags blob. Without +switch_key+,
302
+ # returns true when the whole killswitch is killed. With +switch_key+,
303
+ # returns true when that specific per-key switch is on. Unknown
304
+ # killswitches / switches return false. Not user-scoped.
305
+ def get_killswitch(name, switch_key = nil)
306
+ @telemetry.emit("ks", name)
307
+ ks = @mutex.synchronize { @flags_blob&.dig("killswitches", name.to_s) }
308
+ return false unless ks
309
+ if switch_key.nil?
310
+ Eval.enabled?(ks["killed"])
311
+ else
312
+ Eval.enabled?(ks.dig("switches", switch_key.to_s))
313
+ end
314
+ end
315
+
278
316
  # Batch-evaluate every loaded gate, config and experiment for +user+ into
279
317
  # a bootstrap payload (+{ "flags" => ..., "configs" => ..., "experiments"
280
318
  # => ..., "killswitches" => ... }+) keyed to match the browser SDK's
@@ -576,6 +614,5 @@ module Shipeasy
576
614
  http.read_timeout = 10
577
615
  http.post(uri.request_uri, body, { "X-SDK-Key" => @api_key, "Content-Type" => "text/plain" })
578
616
  end
579
- end
580
617
  end
581
618
  end
@@ -33,7 +33,7 @@ module Shipeasy
33
33
  end
34
34
 
35
35
  # The anon id RackMiddleware resolved for the current request, or nil when
36
- # no middleware ran (e.g. a background job). FlagsClient falls back to this
36
+ # no middleware ran (e.g. a background job). The Engine falls back to this
37
37
  # as the default anonymous_id, so evaluations need no per-call wiring.
38
38
  def current
39
39
  Thread.current[THREAD_KEY]
@@ -11,7 +11,7 @@
11
11
  # require "open_feature/sdk"
12
12
  # require "shipeasy/sdk/openfeature"
13
13
  #
14
- # client = Shipeasy::SDK::FlagsClient.new(api_key: ENV.fetch("SHIPEASY_SERVER_KEY"))
14
+ # client = Shipeasy::Engine.new(api_key: ENV.fetch("SHIPEASY_SERVER_KEY"))
15
15
  # client.init
16
16
  #
17
17
  # OpenFeature::SDK.configure do |config|
@@ -22,7 +22,7 @@
22
22
  # on = of.fetch_boolean_value(flag_key: "new_checkout", default_value: false,
23
23
  # evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "u1"))
24
24
  #
25
- # Pure adapter over `FlagsClient` — no change to evaluation. Boolean values map
25
+ # Pure adapter over `Shipeasy::Engine` — no change to evaluation. Boolean values map
26
26
  # onto gates (`get_flag_detail`); string/number/integer/float/object map onto
27
27
  # dynamic configs (`get_config`).
28
28
 
@@ -37,12 +37,12 @@ rescue LoadError => e
37
37
  "gem \"openfeature-sdk\". (#{e.message})"
38
38
  end
39
39
 
40
- require_relative "flags_client"
40
+ require_relative "../engine"
41
41
 
42
42
  module Shipeasy
43
43
  module OpenFeature
44
44
  # Shipeasy OpenFeature provider (server paradigm). Wraps a
45
- # `Shipeasy::SDK::FlagsClient`; evaluation is local against the cached blob,
45
+ # `Shipeasy::Engine`; evaluation is local against the cached blob,
46
46
  # so resolution is effectively synchronous.
47
47
  class Provider
48
48
  OF = ::OpenFeature::SDK::Provider
@@ -56,12 +56,12 @@ module Shipeasy
56
56
  # FLAG_NOT_FOUND → ERROR (error_code FLAG_NOT_FOUND)
57
57
  # CLIENT_NOT_READY → ERROR (error_code PROVIDER_NOT_READY)
58
58
  REASON_MAP = {
59
- Shipeasy::SDK::FlagsClient::REASON_RULE_MATCH => [OF::Reason::TARGETING_MATCH, nil],
60
- Shipeasy::SDK::FlagsClient::REASON_DEFAULT => [OF::Reason::DEFAULT, nil],
61
- Shipeasy::SDK::FlagsClient::REASON_OFF => [OF::Reason::DISABLED, nil],
62
- Shipeasy::SDK::FlagsClient::REASON_OVERRIDE => [OF::Reason::STATIC, nil],
63
- Shipeasy::SDK::FlagsClient::REASON_FLAG_NOT_FOUND => [OF::Reason::ERROR, OF::ErrorCode::FLAG_NOT_FOUND],
64
- Shipeasy::SDK::FlagsClient::REASON_CLIENT_NOT_READY => [OF::Reason::ERROR, OF::ErrorCode::PROVIDER_NOT_READY],
59
+ Shipeasy::Engine::REASON_RULE_MATCH => [OF::Reason::TARGETING_MATCH, nil],
60
+ Shipeasy::Engine::REASON_DEFAULT => [OF::Reason::DEFAULT, nil],
61
+ Shipeasy::Engine::REASON_OFF => [OF::Reason::DISABLED, nil],
62
+ Shipeasy::Engine::REASON_OVERRIDE => [OF::Reason::STATIC, nil],
63
+ Shipeasy::Engine::REASON_FLAG_NOT_FOUND => [OF::Reason::ERROR, OF::ErrorCode::FLAG_NOT_FOUND],
64
+ Shipeasy::Engine::REASON_CLIENT_NOT_READY => [OF::Reason::ERROR, OF::ErrorCode::PROVIDER_NOT_READY],
65
65
  }.freeze
66
66
 
67
67
  attr_reader :metadata
@@ -1,5 +1,5 @@
1
1
  module Shipeasy
2
2
  module SDK
3
- VERSION = "1.7.0"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
data/lib/shipeasy-sdk.rb CHANGED
@@ -3,7 +3,8 @@ require_relative "shipeasy/config"
3
3
  require_relative "shipeasy/sdk/murmur3"
4
4
  require_relative "shipeasy/sdk/eval"
5
5
  require_relative "shipeasy/sdk/sticky_store"
6
- require_relative "shipeasy/sdk/flags_client"
6
+ require_relative "shipeasy/engine"
7
+ require_relative "shipeasy/client"
7
8
  require_relative "shipeasy/sdk/anon_id"
8
9
  require_relative "shipeasy/sdk/rack_middleware"
9
10
  require_relative "shipeasy/i18n/label_fetcher"
@@ -19,16 +20,16 @@ end
19
20
 
20
21
  module Shipeasy
21
22
  module SDK
22
- # Convenience constructor. Reads api_key + base_url from the gem-wide
23
- # config when omitted, so a single `Shipeasy.configure { }` block at
24
- # boot is enough.
23
+ # Convenience constructor for a heavyweight Engine. Reads api_key + base_url
24
+ # from the gem-wide config when omitted. Most apps should prefer
25
+ # `Shipeasy.configure { … }` + `Shipeasy::Client.new(user)` instead.
25
26
  def self.new_client(api_key: Shipeasy.config.api_key, base_url: Shipeasy.config.base_url)
26
- FlagsClient.new(api_key: api_key, base_url: base_url)
27
+ Shipeasy::Engine.new(api_key: api_key, base_url: base_url)
27
28
  end
28
29
 
29
30
  # ---- see() module-level facade --------------------------------------
30
31
  #
31
- # Backed by a default client, registered when a FlagsClient is constructed
32
+ # Backed by a default client, registered when an Engine is constructed
32
33
  # (last constructed wins). Mirrors the package-level see() in the TS/Python
33
34
  # SDKs so callers can `Shipeasy::SDK.see(e).causes_the(...).to(...)` without
34
35
  # threading a client reference through every call site. A call before any
@@ -38,7 +39,7 @@ module Shipeasy
38
39
  @see_default_mutex = Mutex.new
39
40
 
40
41
  # Register the client backing the module-level see() funcs. Called
41
- # automatically from FlagsClient#initialize; also exposed for explicit use.
42
+ # automatically from Engine#initialize; also exposed for explicit use.
42
43
  def self.set_default_client(client)
43
44
  @see_default_mutex.synchronize { @see_default_client = client }
44
45
  client
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shipeasy-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shipeasy, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-20 00:00:00.000000000 Z
11
+ date: 2026-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -65,13 +65,14 @@ files:
65
65
  - LICENSE
66
66
  - README.md
67
67
  - lib/shipeasy-sdk.rb
68
+ - lib/shipeasy/client.rb
68
69
  - lib/shipeasy/config.rb
70
+ - lib/shipeasy/engine.rb
69
71
  - lib/shipeasy/i18n/label_fetcher.rb
70
72
  - lib/shipeasy/i18n/railtie.rb
71
73
  - lib/shipeasy/i18n/view_helpers.rb
72
74
  - lib/shipeasy/sdk/anon_id.rb
73
75
  - lib/shipeasy/sdk/eval.rb
74
- - lib/shipeasy/sdk/flags_client.rb
75
76
  - lib/shipeasy/sdk/murmur3.rb
76
77
  - lib/shipeasy/sdk/openfeature.rb
77
78
  - lib/shipeasy/sdk/rack_middleware.rb