shipeasy-sdk 1.7.0 → 2.1.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 +87 -268
- data/bin/shipeasy-skill +8 -0
- data/docs/skill/SKILL.md +148 -0
- data/lib/shipeasy/client.rb +71 -0
- data/lib/shipeasy/config.rb +271 -5
- data/lib/shipeasy/{sdk/flags_client.rb → engine.rb} +56 -11
- data/lib/shipeasy/sdk/anon_id.rb +1 -1
- data/lib/shipeasy/sdk/openfeature.rb +42 -15
- data/lib/shipeasy/sdk/skill.rb +76 -0
- data/lib/shipeasy/sdk/version.rb +1 -1
- data/lib/shipeasy-sdk.rb +8 -7
- metadata +9 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f3b0db78c3e6319bafac4913bb823b3dbc24a2e5f7166b1fe9a820f5588fadc
|
|
4
|
+
data.tar.gz: f232c0e140c5891bfd19112e8cea8877b42159806c5aba46185f54bd52b8d3ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 546b88ba03ff9944de78b6982c142b4beba2325dd2b07702bf83518ce7b29778ca2f963c69b3fdfa30fab4a65ff3327ef700666a648e07d4fc2f70d2f57fd03a
|
|
7
|
+
data.tar.gz: eb288754985044ef4d2c3094ce6eb78172eb107394e66a191d6a3740afe167b9b5621a4a4e3ee618d51d62aa1f1f7ce3d1daa873243996cbd4037e62e4ba390f
|
data/README.md
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
This file is GENERATED by scripts/gen_readme.rb from docs/.
|
|
3
|
+
Do NOT edit by hand — edit the docs, then run: ruby scripts/gen_readme.rb
|
|
4
|
+
-->
|
|
5
|
+
|
|
1
6
|
# shipeasy-sdk (Ruby)
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
8
|
+
[](https://github.com/shipeasy-ai/sdk-ruby/actions/workflows/test.yml)
|
|
9
|
+
[](https://rubygems.org/gems/shipeasy-sdk)
|
|
10
|
+
[](https://www.ruby-lang.org/)
|
|
11
|
+
|
|
12
|
+
Server SDK for [Shipeasy](https://shipeasy.ai) — **feature flags, dynamic
|
|
13
|
+
configs, kill switches, A/B experiments, and metric tracking**, with Rails i18n
|
|
14
|
+
view helpers. Server-key only; never embed in a browser.
|
|
15
|
+
|
|
16
|
+
> 📚 **Full documentation:** **<https://shipeasy-ai.github.io/sdk-ruby/>** — also browsable under
|
|
17
|
+
> [`docs/`](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs). This README is generated from those docs.
|
|
5
18
|
|
|
6
19
|
> Source-available under the [Shipeasy-SAL 1.0](./LICENSE).
|
|
7
20
|
|
|
21
|
+
## 🤖 Using an AI agent?
|
|
22
|
+
|
|
23
|
+
This SDK ships an installable **agent skill** — a copy-paste-ready guide to
|
|
24
|
+
`Shipeasy.configure` + `Client.new(user)`, testing, experiments, error
|
|
25
|
+
reporting, and more, with links the agent can pull for deeper docs:
|
|
26
|
+
|
|
27
|
+
- **Skill:** [`docs/skill/SKILL.md`](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/skill/SKILL.md) · raw:
|
|
28
|
+
`https://shipeasy-ai.github.io/sdk-ruby/skill/SKILL.md`
|
|
29
|
+
- **Install it** (ships with the gem — no network):
|
|
30
|
+
`shipeasy-skill install` → `.claude/skills/shipeasy-ruby/SKILL.md`
|
|
31
|
+
(or via the Shipeasy CLI: `shipeasy docs skill --sdk ruby --install`)
|
|
32
|
+
|
|
33
|
+
**Humans:** you can copy that skill straight into your own project's agent skills
|
|
34
|
+
directory (e.g. `.claude/skills/shipeasy-ruby/SKILL.md`) so your coding agent
|
|
35
|
+
always uses the correct Shipeasy patterns. Every doc page and snippet is also
|
|
36
|
+
fetchable by URL — start from the manifest at `https://shipeasy-ai.github.io/sdk-ruby/manifest.json`.
|
|
37
|
+
|
|
8
38
|
## Install
|
|
9
39
|
|
|
10
40
|
```ruby
|
|
@@ -12,297 +42,86 @@ gate evaluation, runtime configs, experiments, and metric ingestion.
|
|
|
12
42
|
gem "shipeasy-sdk"
|
|
13
43
|
```
|
|
14
44
|
|
|
15
|
-
|
|
45
|
+
Requires Ruby 3.0+. Per-framework setup (Rails / Sinatra / serverless) and the
|
|
46
|
+
anon-id middleware are on the [Installation](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/installation.md) page.
|
|
16
47
|
|
|
17
|
-
`
|
|
48
|
+
## Quickstart — `configure` once, then `Client.new(user)` per request
|
|
18
49
|
|
|
19
50
|
```ruby
|
|
51
|
+
# boot (config/initializers/shipeasy.rb)
|
|
20
52
|
Shipeasy.configure do |c|
|
|
21
53
|
c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY")
|
|
22
|
-
c.
|
|
23
|
-
c.profile = "default"
|
|
54
|
+
c.attributes = ->(u) { { "user_id" => u.id, "plan" => u.plan } }
|
|
24
55
|
end
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Anywhere in your app:
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
user = { user_id: current_user.id, plan: current_user.plan }
|
|
31
|
-
|
|
32
|
-
if Shipeasy.flags.get_flag("new_checkout", user)
|
|
33
|
-
# ship it
|
|
34
|
-
end
|
|
35
|
-
|
|
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 })
|
|
39
|
-
```
|
|
40
|
-
|
|
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.
|
|
45
|
-
|
|
46
|
-
In a Rails view (the railtie auto-mounts these helpers when Rails is loaded):
|
|
47
|
-
|
|
48
|
-
```erb
|
|
49
|
-
<%= i18n_head_tags %>
|
|
50
|
-
<h1><%= i18n_t("hero.title", name: current_user.name) %></h1>
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Anonymous visitors (zero-config bucketing)
|
|
54
|
-
|
|
55
|
-
For logged-out traffic you need a *stable* unit so a fractional rollout buckets
|
|
56
|
-
the same on the server and in the browser. In Rails this is automatic: a Railtie
|
|
57
|
-
mounts `Shipeasy::SDK::RackMiddleware`, which mints the shared `__se_anon_id`
|
|
58
|
-
first-party cookie (read + written by every Shipeasy SDK, including the browser)
|
|
59
|
-
for any request without one. Evaluations then default to it with **no per-call
|
|
60
|
-
wiring** — `get_flag` on an anonymous request just works:
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
# current_user is nil → buckets on the __se_anon_id cookie automatically
|
|
64
|
-
Shipeasy.flags.get_flag("new_checkout", {})
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
An explicit `user_id` / `anonymous_id` always wins. If you prefer to read the id
|
|
68
|
-
yourself it's also on the Rack env as `request.env["shipeasy.anon_id"]`. The
|
|
69
|
-
cookie is non-`HttpOnly` by design so the browser SDK can bucket identically. A
|
|
70
|
-
request with **no** unit still resolves a fully-rolled (100%) gate as on; only
|
|
71
|
-
fractional gates need the id. Cookie name + format are a cross-SDK contract —
|
|
72
|
-
see `18-identity-bucketing.md`.
|
|
73
|
-
|
|
74
|
-
For **Sinatra / Hanami / bare Rack** (no Railtie), mount it yourself:
|
|
75
|
-
|
|
76
|
-
```ruby
|
|
77
|
-
use Shipeasy::SDK::RackMiddleware
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Quickstart (plain Ruby / Sinatra / Hanami / scripts)
|
|
81
|
-
|
|
82
|
-
Same pattern, just without `config/initializers`:
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
require "shipeasy-sdk"
|
|
86
|
-
|
|
87
|
-
Shipeasy.configure { |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY") }
|
|
88
|
-
|
|
89
|
-
Shipeasy.flags.get_flag("new_checkout", { user_id: "u_1" })
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The Rails view helpers (`i18n_*`) are not loaded outside Rails, so the
|
|
93
|
-
gem doesn't pull Rails into Sinatra/Hanami apps.
|
|
94
|
-
|
|
95
|
-
## Lambda / Cloud Run / serverless
|
|
96
|
-
|
|
97
|
-
Skip the auto-init facade — it spawns a poll thread you don't want in a
|
|
98
|
-
short-lived function. Build the client explicitly and call `init_once`
|
|
99
|
-
for a single synchronous fetch:
|
|
100
|
-
|
|
101
|
-
```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)
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Lifecycle escape hatch
|
|
108
|
-
|
|
109
|
-
If you want explicit shutdown control in a long-running worker, build the
|
|
110
|
-
client yourself and skip the singleton:
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
client = Shipeasy::SDK.new_client # reads api_key + base_url from Shipeasy.config
|
|
114
|
-
client.init
|
|
115
|
-
at_exit { client.destroy }
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Server-side rendering (SSR)
|
|
119
|
-
|
|
120
|
-
Emit the request's evaluated flags as a declarative `<script>` tag so the
|
|
121
|
-
browser SDK has them on first paint. `bootstrap_script_tag` carries the payload
|
|
122
|
-
in `data-*` attributes (**no key**); the static `se-bootstrap.js` loader
|
|
123
|
-
hydrates `window.__SE_BOOTSTRAP` and writes the `__se_anon_id` cookie so the
|
|
124
|
-
browser buckets identically to the server.
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
user = { "user_id" => "u_123" }
|
|
128
|
-
|
|
129
|
-
# Two tags for the document <head>. The PUBLIC client key (not the server
|
|
130
|
-
# key) goes on the i18n loader tag.
|
|
131
|
-
head = client.bootstrap_script_tag(user, anon_id: anon_id) +
|
|
132
|
-
client.i18n_script_tag(client_key, profile: "en:prod")
|
|
133
|
-
|
|
134
|
-
# …or get the raw payload ({ "flags", "configs", "experiments", "killswitches" }):
|
|
135
|
-
boot = client.evaluate(user)
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
`bootstrap_script_tag` also accepts `i18n_profile:` and `base_url:` (defaults to
|
|
139
|
-
`https://cdn.shipeasy.ai`). In **Rails**, the existing
|
|
140
|
-
`Shipeasy::I18n::ViewHelpers#i18n_script_tag` view helper still renders the i18n
|
|
141
|
-
loader tag from your app config.
|
|
142
|
-
|
|
143
|
-
## Default values
|
|
144
|
-
|
|
145
|
-
`get_flag` and `get_config` take an optional `default:` returned **only when the
|
|
146
|
-
value cannot be resolved** — never when a flag genuinely evaluates to `false`.
|
|
147
|
-
|
|
148
|
-
```ruby
|
|
149
|
-
# Flag: default is returned only when the client isn't ready yet (no blob
|
|
150
|
-
# fetched) or the gate doesn't exist. A gate that evaluates to false (disabled,
|
|
151
|
-
# or outside its rollout) returns false, NOT the default.
|
|
152
|
-
Shipeasy.flags.get_flag("new_checkout", user, default: true)
|
|
153
|
-
|
|
154
|
-
# Config: default is returned when the config key is absent. A decode proc still
|
|
155
|
-
# runs on a present value.
|
|
156
|
-
Shipeasy.flags.get_config("button_color", default: "blue")
|
|
157
|
-
Shipeasy.flags.get_config("limits", ->(v) { v["max"] }, default: 0)
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Evaluation detail
|
|
161
|
-
|
|
162
|
-
`get_flag_detail(name, user)` returns the boolean **and the reason** it was
|
|
163
|
-
reached, as a `FlagDetail` struct (`.value`, `.reason`). `get_flag` is built on
|
|
164
|
-
top of it. The reason is one of the `REASON_*` constants:
|
|
165
|
-
|
|
166
|
-
| Reason | Meaning |
|
|
167
|
-
| ------------------ | --------------------------------------------------- |
|
|
168
|
-
| `OVERRIDE` | answered by a local `override_flag` (no telemetry) |
|
|
169
|
-
| `CLIENT_NOT_READY` | no flag blob fetched/loaded yet |
|
|
170
|
-
| `FLAG_NOT_FOUND` | blob present, but this gate isn't in it |
|
|
171
|
-
| `OFF` | gate present but disabled or killswitched |
|
|
172
|
-
| `RULE_MATCH` | evaluated to `true` |
|
|
173
|
-
| `DEFAULT` | evaluated to `false` (rollout/rule) |
|
|
174
|
-
|
|
175
|
-
```ruby
|
|
176
|
-
detail = Shipeasy.flags.get_flag_detail("new_checkout", user)
|
|
177
|
-
detail.value # => true / false
|
|
178
|
-
detail.reason # => "RULE_MATCH" / "DEFAULT" / "OFF" / ...
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
The `gate` usage beacon fires exactly once per `get_flag_detail` call (never on
|
|
182
|
-
the `OVERRIDE` short-circuit).
|
|
183
56
|
|
|
184
|
-
|
|
57
|
+
# per request — construct once per callsite (cheap; binds the user)
|
|
58
|
+
flags = Shipeasy::Client.new(current_user)
|
|
185
59
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
unsubscribe = Shipeasy.flags.on_change { reload_local_cache! }
|
|
193
|
-
# ... later
|
|
194
|
-
unsubscribe.call
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Offline snapshot
|
|
198
|
-
|
|
199
|
-
For CI, air-gapped runs, or reproducing a production decision from a captured
|
|
200
|
-
blob, build a **no-network** client that still runs the real evaluator against a
|
|
201
|
-
snapshot. The snapshot JSON holds the raw response bodies of the two SDK
|
|
202
|
-
endpoints:
|
|
203
|
-
|
|
204
|
-
```json
|
|
205
|
-
{ "flags": <body of /sdk/flags>, "experiments": <body of /sdk/experiments> }
|
|
60
|
+
flags.get_flag("new_checkout") # NO user arg — bound at construction
|
|
61
|
+
flags.get_config("button_color")
|
|
62
|
+
result = flags.get_experiment("checkout_cta", { label: "Buy" })
|
|
63
|
+
flags.log_exposure("checkout_cta") # at the decision point
|
|
64
|
+
flags.track("purchase", { revenue: 49 }) # on conversion
|
|
65
|
+
flags.get_killswitch("payments")
|
|
206
66
|
```
|
|
207
67
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# or, from already-parsed blobs:
|
|
211
|
-
client = Shipeasy::SDK::FlagsClient.from_snapshot(flags: flags_body, experiments: exps_body)
|
|
212
|
-
|
|
213
|
-
client.get_flag("new_checkout", user) # real evaluation, no network
|
|
214
|
-
client.get_experiment("checkout_cta", user, {})
|
|
215
|
-
```
|
|
68
|
+
Constructing `Shipeasy::Client.new(user)` before `Shipeasy.configure` raises
|
|
69
|
+
`Shipeasy::Error`.
|
|
216
70
|
|
|
217
|
-
|
|
218
|
-
`for_testing` plumbing). Local `override_*` setters still apply on top of the
|
|
219
|
-
snapshot.
|
|
220
|
-
|
|
221
|
-
## Evaluation details
|
|
71
|
+
## Documentation
|
|
222
72
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
-
|
|
232
|
-
|
|
73
|
+
| Page | What |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| [Overview](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/overview.md) | The `Shipeasy.configure` + `Client.new(user)` model. |
|
|
76
|
+
| [Installation](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/installation.md) | Install, frameworks (Rails / Sinatra / serverless), `configure` wiring. |
|
|
77
|
+
| [Configuration](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/configuration.md) | Keys, `attributes`, one-shot vs poll, every option. |
|
|
78
|
+
| [Feature flags](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/flags.md) | `get_flag`, `get_flag_detail`, defaults. |
|
|
79
|
+
| [Dynamic configs](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/configs.md) | `get_config`, typed decode, defaults. |
|
|
80
|
+
| [Kill switches](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/killswitches.md) | `get_killswitch`, named switches. |
|
|
81
|
+
| [Experiments](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/experiments.md) | `get_experiment`, `log_exposure`, `track`. |
|
|
82
|
+
| [Internationalization](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/i18n.md) | Rails view helpers + the SSR loader tag. |
|
|
83
|
+
| [Error reporting](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/error-reporting.md) | `see()` structured error reporting. |
|
|
84
|
+
| [Testing](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/testing.md) | `configure_for_testing` / `configure_for_offline`, overrides. |
|
|
85
|
+
| [OpenFeature](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/openfeature.md) | `Shipeasy::OpenFeature::Provider` (OpenFeature server provider). |
|
|
86
|
+
| [Advanced](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/advanced.md) | Anon-id middleware, private attributes, sticky bucketing, SSR. |
|
|
87
|
+
|
|
88
|
+
Copy-paste snippets live under [`docs/snippets/`](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/snippets)
|
|
89
|
+
(release · metrics · i18n · ops); an installable agent skill is at
|
|
90
|
+
[`docs/skill/SKILL.md`](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/skill/SKILL.md).
|
|
233
91
|
|
|
234
92
|
## Testing
|
|
235
93
|
|
|
236
|
-
|
|
237
|
-
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
|
|
239
|
-
seed each entity with the `override_*` setters (Statsig-style local overrides).
|
|
240
|
-
An override always wins over the fetched blob, so the getters answer
|
|
241
|
-
deterministically:
|
|
94
|
+
Use **`Shipeasy.configure_for_testing`** — the test-mode sibling of [`Shipeasy.configure`](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/configuration.md). It does **zero network**, needs no api key, and seeds the values your code under test should see via override arguments. Then read through the ordinary `Shipeasy::Client.new(user)` — the *same* call your production code uses.
|
|
242
95
|
|
|
243
96
|
```ruby
|
|
244
97
|
require "shipeasy-sdk"
|
|
245
98
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
client.get_flag("new_checkout", { user_id: "u_1" }) # => true
|
|
99
|
+
Shipeasy.configure_for_testing(
|
|
100
|
+
flags: { "new_checkout" => true },
|
|
101
|
+
configs: { "billing_copy" => { "title" => "Welcome" } },
|
|
102
|
+
experiments: { "checkout_button" => ["treatment", { "color" => "green" }] },
|
|
103
|
+
)
|
|
252
104
|
|
|
253
|
-
#
|
|
254
|
-
client.
|
|
255
|
-
client.get_config("button_color") # => "blue"
|
|
256
|
-
client.override_config("limits", { "max" => 10 })
|
|
257
|
-
client.get_config("limits", ->(v) { v["max"] }) # => 10
|
|
105
|
+
# construct once per callsite (cheap; binds the user)
|
|
106
|
+
client = Shipeasy::Client.new({ "user_id" => "u_123" })
|
|
258
107
|
|
|
259
|
-
#
|
|
260
|
-
client.
|
|
261
|
-
r = client.get_experiment("checkout_cta", { user_id: "u_1" }, { label: "default" })
|
|
262
|
-
r.in_experiment # => true
|
|
263
|
-
r.group # => "treatment"
|
|
264
|
-
r.params # => { label: "Buy now" }
|
|
108
|
+
client.get_flag("new_checkout") # => true
|
|
109
|
+
client.get_config("billing_copy") # => { "title" => "Welcome" }
|
|
265
110
|
|
|
266
|
-
|
|
267
|
-
|
|
111
|
+
result = client.get_experiment("checkout_button", { "color" => "blue" })
|
|
112
|
+
result.in_experiment # => true
|
|
113
|
+
result.group # => "treatment"
|
|
114
|
+
result.params # => { "color" => "green" }
|
|
268
115
|
|
|
269
|
-
#
|
|
270
|
-
client.
|
|
116
|
+
# track / log_exposure are no-ops in test mode — safe to call, send nothing
|
|
117
|
+
client.track("purchase", { amount: 49 })
|
|
271
118
|
```
|
|
272
119
|
|
|
273
|
-
|
|
274
|
-
`
|
|
275
|
-
`FlagsClient.new(...)`), so you can pin one value in local development while the
|
|
276
|
-
rest comes from the fetched blob.
|
|
277
|
-
|
|
278
|
-
### Rails singleton
|
|
279
|
-
|
|
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:
|
|
284
|
-
|
|
285
|
-
```ruby
|
|
286
|
-
# RSpec
|
|
287
|
-
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)
|
|
291
|
-
end
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
## Configuration
|
|
295
|
-
|
|
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 |
|
|
300
|
-
|
|
301
|
-
## Documentation
|
|
302
|
-
|
|
303
|
-
[docs.shipeasy.ai](https://docs.shipeasy.ai)
|
|
120
|
+
More — the on-the-spot override helpers and a working example
|
|
121
|
+
`shipeasy-snapshot.json` — on the [Testing](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/testing.md) page.
|
|
304
122
|
|
|
305
123
|
## License
|
|
306
124
|
|
|
307
|
-
[Shipeasy-SAL 1.0](./LICENSE) — source-available, non-commercial-use,
|
|
308
|
-
|
|
125
|
+
[Shipeasy-SAL 1.0](./LICENSE) — source-available, non-commercial-use, permitted
|
|
126
|
+
as a Shipeasy client. Evaluation is tested against the cross-language
|
|
127
|
+
MurmurHash3 vectors in `experiment-platform/04-evaluation.md`.
|
data/bin/shipeasy-skill
ADDED
data/docs/skill/SKILL.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shipeasy-ruby
|
|
3
|
+
description: Use Shipeasy (feature flags, configs, kill switches, A/B experiments, i18n) from Ruby. Covers Shipeasy.configure + Client.new(user), get_flag/get_config/get_experiment/get_killswitch, track, testing, OpenFeature.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Shipeasy Ruby SDK
|
|
7
|
+
|
|
8
|
+
Server-side Ruby gem (`shipeasy-sdk`) for Shipeasy: feature gates, dynamic
|
|
9
|
+
configs, kill switches, A/B experiments, metrics, `see()` error reporting, and
|
|
10
|
+
Rails i18n view helpers. Server-key only — never embed in a browser. Ruby 3.0+.
|
|
11
|
+
|
|
12
|
+
Two things only: **`Shipeasy.configure`** once at boot, then
|
|
13
|
+
**`Shipeasy::Client.new(user)`** per request.
|
|
14
|
+
|
|
15
|
+
> **Pulling deeper docs.** Each section below links its full reference page and
|
|
16
|
+
> copy-paste snippets — fetch any of them as raw Markdown when you need more than
|
|
17
|
+
> this summary. Discover the whole tree from the manifest:
|
|
18
|
+
> `https://shipeasy-ai.github.io/sdk-ruby/manifest.json` (lists every
|
|
19
|
+
> `pages/<key>.md` and `snippets/<group>/<leaf>.md`). All URLs below are
|
|
20
|
+
> `https://shipeasy-ai.github.io/sdk-ruby/…`.
|
|
21
|
+
|
|
22
|
+
## Configure once (boot)
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# config/initializers/shipeasy.rb
|
|
26
|
+
Shipeasy.configure do |c|
|
|
27
|
+
c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY")
|
|
28
|
+
# Optional: map your user object → the attribute hash (runs once in Client.new).
|
|
29
|
+
c.attributes = ->(u) { { "user_id" => u.id, "plan" => u.plan } }
|
|
30
|
+
# i18n only (public client key + profile):
|
|
31
|
+
c.public_key = ENV.fetch("SHIPEASY_CLIENT_KEY")
|
|
32
|
+
c.profile = "default"
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Omit `c.attributes` if your user object is already the attribute hash. For a
|
|
37
|
+
long-running server set `c.poll = true` to keep the blob fresh in the background;
|
|
38
|
+
the default (one-shot fetch, no thread) is serverless-friendly.
|
|
39
|
+
|
|
40
|
+
→ More: `pages/installation.md` (per-framework setup), `pages/configuration.md`
|
|
41
|
+
(every option).
|
|
42
|
+
|
|
43
|
+
## Evaluate (bound `Client.new(user)` — NO user arg)
|
|
44
|
+
|
|
45
|
+
Bind the user once per request, then call without re-passing it — `track` and
|
|
46
|
+
`log_exposure` are on the bound client too, so experiments are end-to-end here:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
flags = Shipeasy::Client.new(current_user) # runs the attributes transform once
|
|
50
|
+
|
|
51
|
+
flags.get_flag("new_checkout") # bool; default: only when unresolved
|
|
52
|
+
flags.get_config("button_color", default: "blue")
|
|
53
|
+
flags.get_killswitch("payments") # true = killed; optional switch_key
|
|
54
|
+
result = flags.get_experiment("checkout_cta", { label: "Buy now" })
|
|
55
|
+
# result.in_experiment / result.group / result.params
|
|
56
|
+
|
|
57
|
+
flags.log_exposure("checkout_cta") # at the decision point
|
|
58
|
+
flags.track("purchase", { revenue: 49 }) # conversion / metric event
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`get_flag_detail` returns a `FlagDetail` (`.value`, `.reason`: `RULE_MATCH`,
|
|
62
|
+
`DEFAULT`, `OFF`, `OVERRIDE`, `FLAG_NOT_FOUND`, `CLIENT_NOT_READY`).
|
|
63
|
+
|
|
64
|
+
→ More: pages `pages/flags.md` · `pages/configs.md` · `pages/killswitches.md`
|
|
65
|
+
(incl. named switches) · `pages/experiments.md`. Snippets
|
|
66
|
+
`snippets/release/{flags,configs,killswitches,experiments}.md` and
|
|
67
|
+
`snippets/metrics/track.md`.
|
|
68
|
+
|
|
69
|
+
## Testing (no network)
|
|
70
|
+
|
|
71
|
+
Use the `configure` siblings — seed overrides, read through the same `Client`:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
Shipeasy.configure_for_testing(
|
|
75
|
+
flags: { "new_checkout" => true },
|
|
76
|
+
configs: { "billing_copy" => { "title" => "Welcome" } },
|
|
77
|
+
experiments: { "checkout_button" => ["treatment", { "color" => "green" }] },
|
|
78
|
+
)
|
|
79
|
+
Shipeasy::Client.new({ "user_id" => "u_123" }).get_flag("new_checkout") # => true
|
|
80
|
+
|
|
81
|
+
# flip a value on the spot, mid-test:
|
|
82
|
+
Shipeasy.override_flag("new_checkout", false)
|
|
83
|
+
Shipeasy.clear_overrides
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Offline (real rules from a snapshot / file):
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
Shipeasy.configure_for_offline(path: "snapshot.json")
|
|
90
|
+
# or snapshot: { "flags" => {...}, "experiments" => {...} }, plus optional overrides
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
→ More: `pages/testing.md` (override helpers + a working example
|
|
94
|
+
`shipeasy-snapshot.json`).
|
|
95
|
+
|
|
96
|
+
## OpenFeature
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
require "open_feature/sdk" # optional dep: gem "openfeature-sdk" (Ruby ≥ 3.4)
|
|
100
|
+
require "shipeasy/sdk/openfeature"
|
|
101
|
+
|
|
102
|
+
Shipeasy.configure { |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY"); c.poll = true }
|
|
103
|
+
OpenFeature::SDK.configure { |c| c.set_provider(Shipeasy::OpenFeature::Provider.new) } # uses the global
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Boolean → gate; string/number/object → config.
|
|
107
|
+
|
|
108
|
+
→ More: `pages/openfeature.md` (reason mapping, type routing).
|
|
109
|
+
|
|
110
|
+
## Error reporting — see()
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
begin
|
|
114
|
+
charge_card(order)
|
|
115
|
+
rescue => e
|
|
116
|
+
Shipeasy.see(e).causes_the("checkout").to("use the backup processor")
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`Shipeasy.see_violation(name)` for non-exception problems;
|
|
121
|
+
`Shipeasy.control_flow_exception(e).because(...)` marks expected control flow
|
|
122
|
+
(reports nothing).
|
|
123
|
+
|
|
124
|
+
→ More: `pages/error-reporting.md` · snippets `snippets/ops/see.md`
|
|
125
|
+
(`.extras`, violations, control-flow exceptions).
|
|
126
|
+
|
|
127
|
+
## i18n (Rails)
|
|
128
|
+
|
|
129
|
+
```erb
|
|
130
|
+
<%= i18n_head_tags %>
|
|
131
|
+
<h1><%= i18n_t("hero.title", name: current_user.name) %></h1>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Outside Rails: `Shipeasy.i18n_script_tag(client_key, profile: "en:prod")` emits
|
|
135
|
+
the loader tag (public client key).
|
|
136
|
+
|
|
137
|
+
→ More: `pages/i18n.md` · snippets `snippets/i18n/{setup,render}.md`.
|
|
138
|
+
|
|
139
|
+
## Other surfaces
|
|
140
|
+
|
|
141
|
+
- Anon bucketing: `Shipeasy::SDK::RackMiddleware` mints the shared `__se_anon_id`
|
|
142
|
+
cookie (Rails Railtie auto-mounts it); anonymous `get_flag` then just works.
|
|
143
|
+
- `c.private_attributes = ["email"]` strips keys from outbound events.
|
|
144
|
+
- `c.sticky_store = Shipeasy::SDK::InMemoryStickyStore.new` pins experiment assignment.
|
|
145
|
+
- SSR: `Shipeasy.bootstrap_script_tag(user)` + `Shipeasy.i18n_script_tag(client_key, "en:prod")`.
|
|
146
|
+
- `Shipeasy.on_change { ... }` (requires `c.poll = true`) fires after a poll fetches new data.
|
|
147
|
+
|
|
148
|
+
→ More: `pages/advanced.md`.
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
|
|
59
|
+
def track(event_name, props = {})
|
|
60
|
+
id = @attributes["user_id"] || @attributes["anonymous_id"]
|
|
61
|
+
@engine.track(id, event_name, props)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def log_exposure(experiment_name)
|
|
65
|
+
@engine.log_exposure(@attributes, experiment_name)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Raised by Shipeasy::Client when constructed before Shipeasy.configure.
|
|
70
|
+
class Error < StandardError; end
|
|
71
|
+
end
|