shipeasy-sdk 2.0.0 → 2.3.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.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -294
  3. data/bin/shipeasy-skill +8 -0
  4. data/docs/skill/SKILL.md +152 -0
  5. data/lib/generators/shipeasy/install/USAGE +24 -0
  6. data/lib/generators/shipeasy/install/install_generator.rb +86 -0
  7. data/lib/generators/shipeasy/install/templates/initializer.rb.tt +41 -0
  8. data/lib/shipeasy/admin.rb +85 -0
  9. data/lib/shipeasy/client.rb +9 -0
  10. data/lib/shipeasy/config.rb +192 -7
  11. data/lib/shipeasy/engine.rb +16 -8
  12. data/lib/shipeasy/sdk/openfeature.rb +33 -6
  13. data/lib/shipeasy/sdk/skill.rb +76 -0
  14. data/lib/shipeasy/sdk/version.rb +1 -1
  15. data/lib/shipeasy_admin/api/alert_rules_api.rb +292 -0
  16. data/lib/shipeasy_admin/api/attributes_api.rb +82 -0
  17. data/lib/shipeasy_admin/api/configs_api.rb +680 -0
  18. data/lib/shipeasy_admin/api/events_api.rb +438 -0
  19. data/lib/shipeasy_admin/api/experiments_api.rb +727 -0
  20. data/lib/shipeasy_admin/api/gates_api.rb +438 -0
  21. data/lib/shipeasy_admin/api/i18n_api.rb +507 -0
  22. data/lib/shipeasy_admin/api/killswitches_api.rb +526 -0
  23. data/lib/shipeasy_admin/api/metrics_api.rb +281 -0
  24. data/lib/shipeasy_admin/api/ops_api.rb +525 -0
  25. data/lib/shipeasy_admin/api/projects_api.rb +153 -0
  26. data/lib/shipeasy_admin/api/universes_api.rb +306 -0
  27. data/lib/shipeasy_admin/api_client.rb +441 -0
  28. data/lib/shipeasy_admin/api_error.rb +58 -0
  29. data/lib/shipeasy_admin/api_model_base.rb +88 -0
  30. data/lib/shipeasy_admin/configuration.rb +397 -0
  31. data/lib/shipeasy_admin/models/approve_event_request.rb +198 -0
  32. data/lib/shipeasy_admin/models/approve_event_response.rb +166 -0
  33. data/lib/shipeasy_admin/models/create_alert_rule_request.rb +408 -0
  34. data/lib/shipeasy_admin/models/create_alert_rule_response.rb +165 -0
  35. data/lib/shipeasy_admin/models/create_config_request.rb +280 -0
  36. data/lib/shipeasy_admin/models/create_config_response.rb +201 -0
  37. data/lib/shipeasy_admin/models/create_event_request.rb +253 -0
  38. data/lib/shipeasy_admin/models/create_event_response.rb +193 -0
  39. data/lib/shipeasy_admin/models/create_experiment_request.rb +869 -0
  40. data/lib/shipeasy_admin/models/create_experiment_request_goal_metric.rb +336 -0
  41. data/lib/shipeasy_admin/models/create_experiment_response.rb +212 -0
  42. data/lib/shipeasy_admin/models/create_gate_request.rb +529 -0
  43. data/lib/shipeasy_admin/models/create_gate_response.rb +212 -0
  44. data/lib/shipeasy_admin/models/create_i18n_profile_request.rb +188 -0
  45. data/lib/shipeasy_admin/models/create_i18n_profile_response.rb +193 -0
  46. data/lib/shipeasy_admin/models/create_killswitch_request.rb +263 -0
  47. data/lib/shipeasy_admin/models/create_killswitch_response.rb +201 -0
  48. data/lib/shipeasy_admin/models/create_metric_request.rb +415 -0
  49. data/lib/shipeasy_admin/models/create_metric_response.rb +193 -0
  50. data/lib/shipeasy_admin/models/create_ops_item_request.rb +287 -0
  51. data/lib/shipeasy_admin/models/create_ops_item_response.rb +177 -0
  52. data/lib/shipeasy_admin/models/create_universe_request.rb +289 -0
  53. data/lib/shipeasy_admin/models/create_universe_response.rb +212 -0
  54. data/lib/shipeasy_admin/models/delete_alert_rule_response.rb +188 -0
  55. data/lib/shipeasy_admin/models/delete_config_response.rb +188 -0
  56. data/lib/shipeasy_admin/models/delete_event_response.rb +189 -0
  57. data/lib/shipeasy_admin/models/delete_experiment_response.rb +188 -0
  58. data/lib/shipeasy_admin/models/delete_gate_response.rb +188 -0
  59. data/lib/shipeasy_admin/models/delete_killswitch_response.rb +188 -0
  60. data/lib/shipeasy_admin/models/delete_metric_response.rb +189 -0
  61. data/lib/shipeasy_admin/models/delete_universe_response.rb +188 -0
  62. data/lib/shipeasy_admin/models/disable_gate_response.rb +190 -0
  63. data/lib/shipeasy_admin/models/discard_config_draft_request.rb +190 -0
  64. data/lib/shipeasy_admin/models/discard_config_draft_response.rb +188 -0
  65. data/lib/shipeasy_admin/models/enable_gate_response.rb +190 -0
  66. data/lib/shipeasy_admin/models/error_response.rb +185 -0
  67. data/lib/shipeasy_admin/models/get_config_response.rb +351 -0
  68. data/lib/shipeasy_admin/models/get_current_project_response.rb +606 -0
  69. data/lib/shipeasy_admin/models/get_current_project_response_module_translations.rb +104 -0
  70. data/lib/shipeasy_admin/models/get_event_response.rb +344 -0
  71. data/lib/shipeasy_admin/models/get_experiment_response.rb +688 -0
  72. data/lib/shipeasy_admin/models/get_experiment_results_response.rb +243 -0
  73. data/lib/shipeasy_admin/models/get_experiment_results_response_experiment.rb +248 -0
  74. data/lib/shipeasy_admin/models/get_experiment_results_response_results_inner.rb +252 -0
  75. data/lib/shipeasy_admin/models/get_experiment_timeseries_response.rb +192 -0
  76. data/lib/shipeasy_admin/models/get_experiment_timeseries_response_experiment.rb +261 -0
  77. data/lib/shipeasy_admin/models/get_experiment_timeseries_response_series_inner.rb +278 -0
  78. data/lib/shipeasy_admin/models/get_killswitch_response.rb +270 -0
  79. data/lib/shipeasy_admin/models/get_metric_response.rb +378 -0
  80. data/lib/shipeasy_admin/models/get_ops_item_response.rb +349 -0
  81. data/lib/shipeasy_admin/models/link_pr_to_ops_item_request.rb +186 -0
  82. data/lib/shipeasy_admin/models/link_pr_to_ops_item_response.rb +166 -0
  83. data/lib/shipeasy_admin/models/list_alert_rules_response_inner.rb +478 -0
  84. data/lib/shipeasy_admin/models/list_alert_rules_response_inner_notify.rb +177 -0
  85. data/lib/shipeasy_admin/models/list_alert_rules_response_inner_notify_slack_channel.rb +211 -0
  86. data/lib/shipeasy_admin/models/list_attributes_response_inner.rb +176 -0
  87. data/lib/shipeasy_admin/models/list_config_activity_response_inner.rb +280 -0
  88. data/lib/shipeasy_admin/models/list_configs_response.rb +178 -0
  89. data/lib/shipeasy_admin/models/list_configs_response_data_inner.rb +351 -0
  90. data/lib/shipeasy_admin/models/list_configs_response_data_inner_drafts_value.rb +237 -0
  91. data/lib/shipeasy_admin/models/list_configs_response_data_inner_envs_value.rb +237 -0
  92. data/lib/shipeasy_admin/models/list_events_response_inner.rb +344 -0
  93. data/lib/shipeasy_admin/models/list_events_response_inner_properties_inner.rb +289 -0
  94. data/lib/shipeasy_admin/models/list_experiments_response.rb +178 -0
  95. data/lib/shipeasy_admin/models/list_experiments_response_data_inner.rb +688 -0
  96. data/lib/shipeasy_admin/models/list_experiments_response_data_inner_groups_inner.rb +258 -0
  97. data/lib/shipeasy_admin/models/list_gates_response.rb +178 -0
  98. data/lib/shipeasy_admin/models/list_gates_response_data_inner.rb +395 -0
  99. data/lib/shipeasy_admin/models/list_gates_response_data_inner_enabled.rb +104 -0
  100. data/lib/shipeasy_admin/models/list_gates_response_data_inner_rules_inner.rb +238 -0
  101. data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner.rb +105 -0
  102. data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner_one_of.rb +375 -0
  103. data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner_one_of1.rb +331 -0
  104. data/lib/shipeasy_admin/models/list_gates_response_data_inner_stack_inner_one_of_ramp.rb +319 -0
  105. data/lib/shipeasy_admin/models/list_i18n_drafts_response_inner.rb +272 -0
  106. data/lib/shipeasy_admin/models/list_i18n_keys_response.rb +195 -0
  107. data/lib/shipeasy_admin/models/list_i18n_keys_response_keys_inner.rb +284 -0
  108. data/lib/shipeasy_admin/models/list_i18n_profiles_response_inner.rb +224 -0
  109. data/lib/shipeasy_admin/models/list_killswitches_response.rb +178 -0
  110. data/lib/shipeasy_admin/models/list_killswitches_response_data_inner.rb +270 -0
  111. data/lib/shipeasy_admin/models/list_killswitches_response_data_inner_envs_value.rb +248 -0
  112. data/lib/shipeasy_admin/models/list_metrics_response_inner.rb +378 -0
  113. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir.rb +295 -0
  114. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg.rb +113 -0
  115. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of.rb +188 -0
  116. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of1.rb +188 -0
  117. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of2.rb +188 -0
  118. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of3.rb +188 -0
  119. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of4.rb +188 -0
  120. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of5.rb +188 -0
  121. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of6.rb +188 -0
  122. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of7.rb +217 -0
  123. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of8.rb +233 -0
  124. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of9.rb +240 -0
  125. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of9_numerator.rb +266 -0
  126. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_agg_one_of9_numerator_filters_inner.rb +264 -0
  127. data/lib/shipeasy_admin/models/list_metrics_response_inner_query_ir_group_by.rb +228 -0
  128. data/lib/shipeasy_admin/models/list_ops_items_response_inner.rb +349 -0
  129. data/lib/shipeasy_admin/models/list_slack_channels_response.rb +195 -0
  130. data/lib/shipeasy_admin/models/list_slack_channels_response_channels_inner.rb +203 -0
  131. data/lib/shipeasy_admin/models/list_universes_response.rb +178 -0
  132. data/lib/shipeasy_admin/models/list_universes_response_data_inner.rb +305 -0
  133. data/lib/shipeasy_admin/models/notify_ops_request.rb +261 -0
  134. data/lib/shipeasy_admin/models/notify_ops_response.rb +193 -0
  135. data/lib/shipeasy_admin/models/publish_config_draft_request.rb +190 -0
  136. data/lib/shipeasy_admin/models/publish_config_draft_response.rb +260 -0
  137. data/lib/shipeasy_admin/models/publish_i18n_profile_request.rb +149 -0
  138. data/lib/shipeasy_admin/models/publish_i18n_profile_response.rb +404 -0
  139. data/lib/shipeasy_admin/models/push_i18n_keys_request.rb +205 -0
  140. data/lib/shipeasy_admin/models/push_i18n_keys_request_keys_inner.rb +192 -0
  141. data/lib/shipeasy_admin/models/push_i18n_keys_response.rb +261 -0
  142. data/lib/shipeasy_admin/models/reanalyze_experiment_response.rb +214 -0
  143. data/lib/shipeasy_admin/models/save_config_draft_request.rb +202 -0
  144. data/lib/shipeasy_admin/models/save_config_draft_response.rb +286 -0
  145. data/lib/shipeasy_admin/models/set_experiment_metrics_request.rb +168 -0
  146. data/lib/shipeasy_admin/models/set_experiment_metrics_request_metrics_inner.rb +216 -0
  147. data/lib/shipeasy_admin/models/set_experiment_metrics_response.rb +179 -0
  148. data/lib/shipeasy_admin/models/set_experiment_metrics_response_metrics_inner.rb +201 -0
  149. data/lib/shipeasy_admin/models/set_experiment_status_request.rb +190 -0
  150. data/lib/shipeasy_admin/models/set_experiment_status_response.rb +214 -0
  151. data/lib/shipeasy_admin/models/set_killswitch_switch_request.rb +264 -0
  152. data/lib/shipeasy_admin/models/set_killswitch_switch_response.rb +288 -0
  153. data/lib/shipeasy_admin/models/unset_killswitch_switch_request.rb +237 -0
  154. data/lib/shipeasy_admin/models/unset_killswitch_switch_response.rb +289 -0
  155. data/lib/shipeasy_admin/models/update_alert_rule_request.rb +306 -0
  156. data/lib/shipeasy_admin/models/update_alert_rule_response.rb +165 -0
  157. data/lib/shipeasy_admin/models/update_config_request.rb +198 -0
  158. data/lib/shipeasy_admin/models/update_config_response.rb +165 -0
  159. data/lib/shipeasy_admin/models/update_event_request.rb +198 -0
  160. data/lib/shipeasy_admin/models/update_event_response.rb +166 -0
  161. data/lib/shipeasy_admin/models/update_experiment_request.rb +746 -0
  162. data/lib/shipeasy_admin/models/update_experiment_response.rb +165 -0
  163. data/lib/shipeasy_admin/models/update_gate_request.rb +403 -0
  164. data/lib/shipeasy_admin/models/update_gate_response.rb +166 -0
  165. data/lib/shipeasy_admin/models/update_i18n_key_request.rb +176 -0
  166. data/lib/shipeasy_admin/models/update_i18n_key_response.rb +166 -0
  167. data/lib/shipeasy_admin/models/update_killswitch_request.rb +224 -0
  168. data/lib/shipeasy_admin/models/update_killswitch_response.rb +165 -0
  169. data/lib/shipeasy_admin/models/update_ops_item_request.rb +205 -0
  170. data/lib/shipeasy_admin/models/update_ops_item_response.rb +166 -0
  171. data/lib/shipeasy_admin/models/update_universe_request.rb +213 -0
  172. data/lib/shipeasy_admin/models/update_universe_response.rb +165 -0
  173. data/lib/shipeasy_admin/models/upsert_project_request.rb +222 -0
  174. data/lib/shipeasy_admin/models/upsert_project_response.rb +260 -0
  175. data/lib/shipeasy_admin/version.rb +15 -0
  176. data/lib/shipeasy_admin.rb +196 -0
  177. metadata +235 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97efde389fa7a5adc1ffdab3951d09837f435bd529d8f6c3b7e3d45387472628
4
- data.tar.gz: 46f183776d94ccbddd8781e39d3ea5e56e6795cf26f3292f1482c2886b7d0836
3
+ metadata.gz: 434e056f9f0849c73316e812f9ff8918ad838d9161283db0b3a8c9c7450b1077
4
+ data.tar.gz: beb6a893abc08ec7463a64fb2ec22f7df014f4da147b824a5f7b8d70ed3f34a0
5
5
  SHA512:
6
- metadata.gz: 010654d1322131ab21b467fdff8176914e427558a5a620762c4417378f68c5cc5f3b189e7b0cdb8de6b741d2232e7c1552b8fd551bc3fce19b09e91263e18d12
7
- data.tar.gz: 24730826e8f2de0ac3f1546ab4f42b2301bcd8f83bdaea0b9c50487d7b90d5b445470827199de73c4794bc16b2ddeed3e18fc44079c2000cd0c92aeb07f50e55
6
+ metadata.gz: 17339d55c4ed50599d3cc2cfa04f2f5a53e57a5478fb626bbf999ac6297da3a35a47e9a139150b4cfcce209c6e29f1b033180c9f5cc64923f106f5c1379adff8
7
+ data.tar.gz: 57fb7a408683111fd2f0aee2b389c97f5de038b22b2b6bbf0ff757c193220740a766951a7cda8baa59c58fc83bc26ea7e1103a07944e9206555a22970d071b19
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
- Ruby gem for the [Shipeasy](https://shipeasy.ai) hosted service. Server-side
4
- gate evaluation, runtime configs, experiments, and metric ingestion.
8
+ [![Tests](https://github.com/shipeasy-ai/sdk-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/shipeasy-ai/sdk-ruby/actions/workflows/test.yml)
9
+ [![Gem](https://img.shields.io/gem/v/shipeasy-sdk.svg)](https://rubygems.org/gems/shipeasy-sdk)
10
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-CC342D.svg)](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,324 +42,87 @@ gate evaluation, runtime configs, experiments, and metric ingestion.
12
42
  gem "shipeasy-sdk"
13
43
  ```
14
44
 
15
- ## Quickstart (Rails)
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
- 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`:
48
+ ## Quickstart — `configure` once, then `Client.new(user)` per request
21
49
 
22
50
  ```ruby
51
+ # boot (config/initializers/shipeasy.rb)
23
52
  Shipeasy.configure do |c|
24
53
  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
54
  c.attributes = ->(u) { { "user_id" => u.id, "plan" => u.plan } }
30
-
31
- c.public_key = ENV.fetch("SHIPEASY_CLIENT_KEY") # for i18n view helpers
32
- c.profile = "default"
33
- end
34
- ```
35
-
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:
39
-
40
- ```ruby
41
- flags = Shipeasy::Client.new(current_user) # runs the attributes transform once
42
-
43
- if flags.get_flag("new_checkout") # NO user arg
44
- # ship it
45
55
  end
46
56
 
47
- color = flags.get_config("button_color")
48
- result = flags.get_experiment("checkout_cta", { label: "Buy now" })
49
- panic = flags.get_killswitch("payments")
50
- ```
57
+ # per request — construct once per callsite (cheap; binds the user)
58
+ flags = Shipeasy::Client.new(current_user)
51
59
 
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 })
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")
61
66
  ```
62
67
 
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.
68
+ Constructing `Shipeasy::Client.new(user)` before `Shipeasy.configure` raises
69
+ `Shipeasy::Error`.
67
70
 
68
- In a Rails view (the railtie auto-mounts these helpers when Rails is loaded):
69
-
70
- ```erb
71
- <%= i18n_head_tags %>
72
- <h1><%= i18n_t("hero.title", name: current_user.name) %></h1>
73
- ```
74
-
75
- ### Anonymous visitors (zero-config bucketing)
76
-
77
- For logged-out traffic you need a *stable* unit so a fractional rollout buckets
78
- the same on the server and in the browser. In Rails this is automatic: a Railtie
79
- mounts `Shipeasy::SDK::RackMiddleware`, which mints the shared `__se_anon_id`
80
- first-party cookie (read + written by every Shipeasy SDK, including the browser)
81
- for any request without one. Evaluations then default to it with **no per-call
82
- wiring** — `get_flag` on an anonymous request just works:
83
-
84
- ```ruby
85
- # current_user is nil → buckets on the __se_anon_id cookie automatically
86
- Shipeasy::Client.new({}).get_flag("new_checkout")
87
- ```
88
-
89
- An explicit `user_id` / `anonymous_id` always wins. If you prefer to read the id
90
- yourself it's also on the Rack env as `request.env["shipeasy.anon_id"]`. The
91
- cookie is non-`HttpOnly` by design so the browser SDK can bucket identically. A
92
- request with **no** unit still resolves a fully-rolled (100%) gate as on; only
93
- fractional gates need the id. Cookie name + format are a cross-SDK contract —
94
- see `18-identity-bucketing.md`.
95
-
96
- For **Sinatra / Hanami / bare Rack** (no Railtie), mount it yourself:
97
-
98
- ```ruby
99
- use Shipeasy::SDK::RackMiddleware
100
- ```
101
-
102
- ## Quickstart (plain Ruby / Sinatra / Hanami / scripts)
103
-
104
- Same pattern, just without `config/initializers`:
105
-
106
- ```ruby
107
- require "shipeasy-sdk"
108
-
109
- Shipeasy.configure { |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY") }
110
-
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")
113
- ```
114
-
115
- The Rails view helpers (`i18n_*`) are not loaded outside Rails, so the
116
- gem doesn't pull Rails into Sinatra/Hanami apps.
117
-
118
- ## Lambda / Cloud Run / serverless
119
-
120
- Skip the auto-init facade — it spawns a poll thread you don't want in a
121
- short-lived function. Build the client explicitly and call `init_once`
122
- for a single synchronous fetch:
123
-
124
- ```ruby
125
- engine = Shipeasy::Engine.new(api_key: ENV.fetch("SHIPEASY_SERVER_KEY"))
126
- engine.init_once
127
- engine.get_flag("new_checkout", user)
128
- ```
129
-
130
- ## Lifecycle escape hatch
131
-
132
- If you want explicit shutdown control in a long-running worker, build the
133
- client yourself and skip the singleton:
134
-
135
- ```ruby
136
- client = Shipeasy::SDK.new_client # reads api_key + base_url from Shipeasy.config
137
- client.init
138
- at_exit { client.destroy }
139
- ```
140
-
141
- ## Server-side rendering (SSR)
142
-
143
- Emit the request's evaluated flags as a declarative `<script>` tag so the
144
- browser SDK has them on first paint. `bootstrap_script_tag` carries the payload
145
- in `data-*` attributes (**no key**); the static `se-bootstrap.js` loader
146
- hydrates `window.__SE_BOOTSTRAP` and writes the `__se_anon_id` cookie so the
147
- browser buckets identically to the server.
148
-
149
- ```ruby
150
- user = { "user_id" => "u_123" }
151
-
152
- # Two tags for the document <head>. The PUBLIC client key (not the server
153
- # key) goes on the i18n loader tag.
154
- head = client.bootstrap_script_tag(user, anon_id: anon_id) +
155
- client.i18n_script_tag(client_key, profile: "en:prod")
156
-
157
- # …or get the raw payload ({ "flags", "configs", "experiments", "killswitches" }):
158
- boot = client.evaluate(user)
159
- ```
160
-
161
- `bootstrap_script_tag` also accepts `i18n_profile:` and `base_url:` (defaults to
162
- `https://cdn.shipeasy.ai`). In **Rails**, the existing
163
- `Shipeasy::I18n::ViewHelpers#i18n_script_tag` view helper still renders the i18n
164
- loader tag from your app config.
165
-
166
- ## Default values
167
-
168
- `get_flag` and `get_config` take an optional `default:` returned **only when the
169
- value cannot be resolved** — never when a flag genuinely evaluates to `false`.
170
-
171
- ```ruby
172
- # Flag: default is returned only when the client isn't ready yet (no blob
173
- # fetched) or the gate doesn't exist. A gate that evaluates to false (disabled,
174
- # or outside its rollout) returns false, NOT the default.
175
- Shipeasy.flags.get_flag("new_checkout", user, default: true)
176
-
177
- # Config: default is returned when the config key is absent. A decode proc still
178
- # runs on a present value.
179
- Shipeasy.flags.get_config("button_color", default: "blue")
180
- Shipeasy.flags.get_config("limits", ->(v) { v["max"] }, default: 0)
181
- ```
182
-
183
- ## Evaluation detail
184
-
185
- `get_flag_detail(name, user)` returns the boolean **and the reason** it was
186
- reached, as a `FlagDetail` struct (`.value`, `.reason`). `get_flag` is built on
187
- top of it. The reason is one of the `REASON_*` constants:
188
-
189
- | Reason | Meaning |
190
- | ------------------ | --------------------------------------------------- |
191
- | `OVERRIDE` | answered by a local `override_flag` (no telemetry) |
192
- | `CLIENT_NOT_READY` | no flag blob fetched/loaded yet |
193
- | `FLAG_NOT_FOUND` | blob present, but this gate isn't in it |
194
- | `OFF` | gate present but disabled or killswitched |
195
- | `RULE_MATCH` | evaluated to `true` |
196
- | `DEFAULT` | evaluated to `false` (rollout/rule) |
197
-
198
- ```ruby
199
- detail = Shipeasy.flags.get_flag_detail("new_checkout", user)
200
- detail.value # => true / false
201
- detail.reason # => "RULE_MATCH" / "DEFAULT" / "OFF" / ...
202
- ```
203
-
204
- The `gate` usage beacon fires exactly once per `get_flag_detail` call (never on
205
- the `OVERRIDE` short-circuit).
206
-
207
- ## Change listeners
208
-
209
- `on_change` registers a callback fired after a background poll fetches **new**
210
- flag/config data (HTTP 200, not a 304). It accepts a block or any callable and
211
- returns an unsubscribe proc. Listeners never fire in test/offline mode (there is
212
- no poll thread). A raising listener is isolated and logged, not propagated.
213
-
214
- ```ruby
215
- unsubscribe = Shipeasy.flags.on_change { reload_local_cache! }
216
- # ... later
217
- unsubscribe.call
218
- ```
219
-
220
- ## Offline snapshot
221
-
222
- For CI, air-gapped runs, or reproducing a production decision from a captured
223
- blob, build a **no-network** client that still runs the real evaluator against a
224
- snapshot. The snapshot JSON holds the raw response bodies of the two SDK
225
- endpoints:
226
-
227
- ```json
228
- { "flags": <body of /sdk/flags>, "experiments": <body of /sdk/experiments> }
229
- ```
230
-
231
- ```ruby
232
- client = Shipeasy::Engine.from_file("snapshot.json")
233
- # or, from already-parsed blobs:
234
- client = Shipeasy::Engine.from_snapshot(flags: flags_body, experiments: exps_body)
235
-
236
- client.get_flag("new_checkout", user) # real evaluation, no network
237
- client.get_experiment("checkout_cta", user, {})
238
- ```
239
-
240
- `init` / `init_once` / `track` are no-ops and telemetry is off (it reuses the
241
- `for_testing` plumbing). Local `override_*` setters still apply on top of the
242
- snapshot.
243
-
244
- ## Evaluation details
71
+ ## Documentation
245
72
 
246
- - **Gates** rules matched in order; rollout bucket =
247
- `murmur3("#{salt}:#{uid}") % 10000 < rollout_pct`.
248
- - **Experiments** `status == "running"`, optional targeting gate,
249
- universe holdout range, allocation bucket, then group assignment by
250
- weight.
251
- - **MurmurHash3** — pure-Ruby x86_32 variant, seed 0.
252
- - **ETag caching** each poll sends `If-None-Match`; a 304 skips the
253
- JSON parse.
254
- - **Poll interval** defaults to 30 s; overridden by the
255
- `X-Poll-Interval` header from the flags endpoint.
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
+ | [Admin API client](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/admin-api.md) | Optional `Shipeasy::Admin::Client` for administering resources (generated from the Open… |
88
+
89
+ Copy-paste snippets live under [`docs/snippets/`](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/snippets)
90
+ (release · metrics · i18n · ops); an installable agent skill is at
91
+ [`docs/skill/SKILL.md`](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/skill/SKILL.md).
256
92
 
257
93
  ## Testing
258
94
 
259
- For unit/integration tests you want a client that does **zero network** and
260
- returns exactly the values you seed — no api_key, no fetch, no poll thread, no
261
- telemetry, no metric ingestion. Build one with `Shipeasy::Engine.for_testing` and
262
- seed each entity with the `override_*` setters (Statsig-style local overrides).
263
- An override always wins over the fetched blob, so the getters answer
264
- deterministically:
95
+ 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.
265
96
 
266
97
  ```ruby
267
98
  require "shipeasy-sdk"
268
99
 
269
- client = Shipeasy::Engine.for_testing
270
- # init / init_once are no-ops here — nothing is ever fetched.
271
-
272
- # Flags (boolean)
273
- client.override_flag("new_checkout", true)
274
- client.get_flag("new_checkout", { user_id: "u_1" }) # => true
100
+ Shipeasy.configure_for_testing(
101
+ flags: { "new_checkout" => true },
102
+ configs: { "billing_copy" => { "title" => "Welcome" } },
103
+ experiments: { "checkout_button" => ["treatment", { "color" => "green" }] },
104
+ )
275
105
 
276
- # Configs (any value; an optional decode proc still runs)
277
- client.override_config("button_color", "blue")
278
- client.get_config("button_color") # => "blue"
279
- client.override_config("limits", { "max" => 10 })
280
- client.get_config("limits", ->(v) { v["max"] }) # => 10
106
+ # construct once per callsite (cheap; binds the user)
107
+ client = Shipeasy::Client.new({ "user_id" => "u_123" })
281
108
 
282
- # Experiments — returns an in-experiment Eval::ExperimentResult
283
- client.override_experiment("checkout_cta", "treatment", { label: "Buy now" })
284
- r = client.get_experiment("checkout_cta", { user_id: "u_1" }, { label: "default" })
285
- r.in_experiment # => true
286
- r.group # => "treatment"
287
- r.params # => { label: "Buy now" }
109
+ client.get_flag("new_checkout") # => true
110
+ client.get_config("billing_copy") # => { "title" => "Welcome" }
288
111
 
289
- # track is a no-op (no thread, no network) assert call counts without stubbing.
290
- client.track("u_1", "checkout_completed", { revenue: 49.99 }) # => nil
112
+ result = client.get_experiment("checkout_button", { "color" => "blue" })
113
+ result.in_experiment # => true
114
+ result.group # => "treatment"
115
+ result.params # => { "color" => "green" }
291
116
 
292
- # Reset between examples
293
- client.clear_overrides
117
+ # track / log_exposure are no-ops in test mode — safe to call, send nothing
118
+ client.track("purchase", { amount: 49 })
294
119
  ```
295
120
 
296
- The same `override_flag` / `override_config` / `override_experiment` /
297
- `clear_overrides` setters also work on a **normal** live client (built with
298
- `Shipeasy::Engine.new(...)`), so you can pin one value in local development while the
299
- rest comes from the fetched blob.
300
-
301
- ### Global engine / bound client
302
-
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:
307
-
308
- ```ruby
309
- # RSpec
310
- before do
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
317
- end
318
- ```
319
-
320
- ## Configuration
321
-
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 |
327
-
328
- ## Documentation
329
-
330
- [docs.shipeasy.ai](https://docs.shipeasy.ai)
121
+ More the on-the-spot override helpers and a working example
122
+ `shipeasy-snapshot.json` on the [Testing](https://github.com/shipeasy-ai/sdk-ruby/blob/main/docs/pages/testing.md) page.
331
123
 
332
124
  ## License
333
125
 
334
- [Shipeasy-SAL 1.0](./LICENSE) — source-available, non-commercial-use,
335
- permitted as a Shipeasy client.
126
+ [Shipeasy-SAL 1.0](./LICENSE) — source-available, non-commercial-use, permitted
127
+ as a Shipeasy client. Evaluation is tested against the cross-language
128
+ MurmurHash3 vectors in `experiment-platform/04-evaluation.md`.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Opt-in installer for the bundled Shipeasy agent skill. See
5
+ # lib/shipeasy/sdk/skill.rb.
6
+ require "shipeasy/sdk/skill"
7
+
8
+ exit Shipeasy::SDK::Skill.main(ARGV)
@@ -0,0 +1,152 @@
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
+ **Rails:** don't write the initializer by hand — run the generator:
41
+ `bin/rails generate shipeasy:install` (add `--i18n` to also inject
42
+ `<%= i18n_head_tags %>` into the app layout, `--no-poll` for serverless).
43
+
44
+ → More: `pages/installation.md` (per-framework setup + generator),
45
+ `pages/configuration.md` (every option).
46
+
47
+ ## Evaluate (bound `Client.new(user)` — NO user arg)
48
+
49
+ Bind the user once per request, then call without re-passing it — `track` and
50
+ `log_exposure` are on the bound client too, so experiments are end-to-end here:
51
+
52
+ ```ruby
53
+ flags = Shipeasy::Client.new(current_user) # runs the attributes transform once
54
+
55
+ flags.get_flag("new_checkout") # bool; default: only when unresolved
56
+ flags.get_config("button_color", default: "blue")
57
+ flags.get_killswitch("payments") # true = killed; optional switch_key
58
+ result = flags.get_experiment("checkout_cta", { label: "Buy now" })
59
+ # result.in_experiment / result.group / result.params
60
+
61
+ flags.log_exposure("checkout_cta") # at the decision point
62
+ flags.track("purchase", { revenue: 49 }) # conversion / metric event
63
+ ```
64
+
65
+ `get_flag_detail` returns a `FlagDetail` (`.value`, `.reason`: `RULE_MATCH`,
66
+ `DEFAULT`, `OFF`, `OVERRIDE`, `FLAG_NOT_FOUND`, `CLIENT_NOT_READY`).
67
+
68
+ → More: pages `pages/flags.md` · `pages/configs.md` · `pages/killswitches.md`
69
+ (incl. named switches) · `pages/experiments.md`. Snippets
70
+ `snippets/release/{flags,configs,killswitches,experiments}.md` and
71
+ `snippets/metrics/track.md`.
72
+
73
+ ## Testing (no network)
74
+
75
+ Use the `configure` siblings — seed overrides, read through the same `Client`:
76
+
77
+ ```ruby
78
+ Shipeasy.configure_for_testing(
79
+ flags: { "new_checkout" => true },
80
+ configs: { "billing_copy" => { "title" => "Welcome" } },
81
+ experiments: { "checkout_button" => ["treatment", { "color" => "green" }] },
82
+ )
83
+ Shipeasy::Client.new({ "user_id" => "u_123" }).get_flag("new_checkout") # => true
84
+
85
+ # flip a value on the spot, mid-test:
86
+ Shipeasy.override_flag("new_checkout", false)
87
+ Shipeasy.clear_overrides
88
+ ```
89
+
90
+ Offline (real rules from a snapshot / file):
91
+
92
+ ```ruby
93
+ Shipeasy.configure_for_offline(path: "snapshot.json")
94
+ # or snapshot: { "flags" => {...}, "experiments" => {...} }, plus optional overrides
95
+ ```
96
+
97
+ → More: `pages/testing.md` (override helpers + a working example
98
+ `shipeasy-snapshot.json`).
99
+
100
+ ## OpenFeature
101
+
102
+ ```ruby
103
+ require "open_feature/sdk" # optional dep: gem "openfeature-sdk" (Ruby ≥ 3.4)
104
+ require "shipeasy/sdk/openfeature"
105
+
106
+ Shipeasy.configure { |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY"); c.poll = true }
107
+ OpenFeature::SDK.configure { |c| c.set_provider(Shipeasy::OpenFeature::Provider.new) } # uses the global
108
+ ```
109
+
110
+ Boolean → gate; string/number/object → config.
111
+
112
+ → More: `pages/openfeature.md` (reason mapping, type routing).
113
+
114
+ ## Error reporting — see()
115
+
116
+ ```ruby
117
+ begin
118
+ charge_card(order)
119
+ rescue => e
120
+ Shipeasy.see(e).causes_the("checkout").to("use the backup processor")
121
+ end
122
+ ```
123
+
124
+ `Shipeasy.see_violation(name)` for non-exception problems;
125
+ `Shipeasy.control_flow_exception(e).because(...)` marks expected control flow
126
+ (reports nothing).
127
+
128
+ → More: `pages/error-reporting.md` · snippets `snippets/ops/see.md`
129
+ (`.extras`, violations, control-flow exceptions).
130
+
131
+ ## i18n (Rails)
132
+
133
+ ```erb
134
+ <%= i18n_head_tags %>
135
+ <h1><%= i18n_t("hero.title", name: current_user.name) %></h1>
136
+ ```
137
+
138
+ Outside Rails: `Shipeasy.i18n_script_tag(client_key, profile: "en:prod")` emits
139
+ the loader tag (public client key).
140
+
141
+ → More: `pages/i18n.md` · snippets `snippets/i18n/{setup,render}.md`.
142
+
143
+ ## Other surfaces
144
+
145
+ - Anon bucketing: `Shipeasy::SDK::RackMiddleware` mints the shared `__se_anon_id`
146
+ cookie (Rails Railtie auto-mounts it); anonymous `get_flag` then just works.
147
+ - `c.private_attributes = ["email"]` strips keys from outbound events.
148
+ - `c.sticky_store = Shipeasy::SDK::InMemoryStickyStore.new` pins experiment assignment.
149
+ - SSR: `Shipeasy.bootstrap_script_tag(user)` + `Shipeasy.i18n_script_tag(client_key, "en:prod")`.
150
+ - `Shipeasy.on_change { ... }` (requires `c.poll = true`) fires after a poll fetches new data.
151
+
152
+ → More: `pages/advanced.md`.
@@ -0,0 +1,24 @@
1
+ Description:
2
+ Install Shipeasy into a Rails app. Creates the config/initializers/shipeasy.rb
3
+ initializer that calls `Shipeasy.configure` once at boot. With --i18n it also
4
+ sets the public client key and injects `<%= i18n_head_tags %>` into your
5
+ application layout's <head>.
6
+
7
+ The gem's Railties already auto-mount the anon-id Rack middleware and the i18n
8
+ view helpers, so this generator only creates what your app must own — the
9
+ initializer, the (optional) layout tag, and a reminder to set your keys.
10
+
11
+ Example:
12
+ rails generate shipeasy:install
13
+
14
+ Creates:
15
+ config/initializers/shipeasy.rb (server key + background poll)
16
+
17
+ rails generate shipeasy:install --i18n
18
+
19
+ Also sets the public client key in the initializer and injects
20
+ <%= i18n_head_tags %> into app/views/layouts/application.html.erb
21
+
22
+ rails generate shipeasy:install --no-poll
23
+
24
+ One-shot fetch instead of the background poll (serverless / Lambda).