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 +4 -4
- data/README.md +60 -33
- data/lib/shipeasy/client.rb +62 -0
- data/lib/shipeasy/config.rb +86 -5
- data/lib/shipeasy/{sdk/flags_client.rb → engine.rb} +46 -9
- data/lib/shipeasy/sdk/anon_id.rb +1 -1
- data/lib/shipeasy/sdk/openfeature.rb +10 -10
- data/lib/shipeasy/sdk/version.rb +1 -1
- data/lib/shipeasy-sdk.rb +8 -7
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97efde389fa7a5adc1ffdab3951d09837f435bd529d8f6c3b7e3d45387472628
|
|
4
|
+
data.tar.gz: 46f183776d94ccbddd8781e39d3ea5e56e6795cf26f3292f1482c2886b7d0836
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
flags = Shipeasy::Client.new(current_user) # runs the attributes transform once
|
|
31
42
|
|
|
32
|
-
if
|
|
43
|
+
if flags.get_flag("new_checkout") # NO user arg
|
|
33
44
|
# ship it
|
|
34
45
|
end
|
|
35
46
|
|
|
36
|
-
color =
|
|
37
|
-
result =
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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::
|
|
232
|
+
client = Shipeasy::Engine.from_file("snapshot.json")
|
|
210
233
|
# or, from already-parsed blobs:
|
|
211
|
-
client = Shipeasy::
|
|
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 `
|
|
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::
|
|
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
|
-
`
|
|
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
|
-
###
|
|
301
|
+
### Global engine / bound client
|
|
279
302
|
|
|
280
|
-
`Shipeasy.
|
|
281
|
-
|
|
282
|
-
`Shipeasy.
|
|
283
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
allow(Shipeasy).to receive(:
|
|
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
|
|
297
|
-
|
|
|
298
|
-
| `api_key`
|
|
299
|
-
| `base_url`
|
|
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
|
data/lib/shipeasy/config.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
#
|
|
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
|
data/lib/shipeasy/sdk/anon_id.rb
CHANGED
|
@@ -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).
|
|
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::
|
|
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 `
|
|
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 "
|
|
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::
|
|
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::
|
|
60
|
-
Shipeasy::
|
|
61
|
-
Shipeasy::
|
|
62
|
-
Shipeasy::
|
|
63
|
-
Shipeasy::
|
|
64
|
-
Shipeasy::
|
|
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
|
data/lib/shipeasy/sdk/version.rb
CHANGED
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/
|
|
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
|
|
23
|
-
# config when omitted
|
|
24
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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-
|
|
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
|