shipeasy-sdk 1.6.0 → 1.7.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 +25 -0
- data/lib/shipeasy/sdk/flags_client.rb +77 -0
- data/lib/shipeasy/sdk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afdce684e3c6a83b220b7dfaa1d7db249284e3bb4c4ebfe3c9b27c54da0d801b
|
|
4
|
+
data.tar.gz: ecdfdf8aed19f1b5f18fe874ce5b13064bb61abe7bf7f1c47a76b08d9ab99bce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6a5470268bc584eaa2dbc8c89f722a22633d0bd6f896aaa1d73db74090b56667c87733c614b8a49bdeda88dab043740509153503943837a69be20a03dc315b7
|
|
7
|
+
data.tar.gz: 55289a4adf0559f41eca2d1f6bca03b53a19da9b3f00f4109dc4476ed0c5cbaaed453dbaf3f7c0ebcc1405f0d73a55fe702e02859c9a809d525e92b294e275ba
|
data/README.md
CHANGED
|
@@ -115,6 +115,31 @@ client.init
|
|
|
115
115
|
at_exit { client.destroy }
|
|
116
116
|
```
|
|
117
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
|
+
|
|
118
143
|
## Default values
|
|
119
144
|
|
|
120
145
|
`get_flag` and `get_config` take an optional `default:` returned **only when the
|
|
@@ -2,6 +2,7 @@ require "net/http"
|
|
|
2
2
|
require "uri"
|
|
3
3
|
require "json"
|
|
4
4
|
require "thread"
|
|
5
|
+
require "cgi"
|
|
5
6
|
require_relative "eval"
|
|
6
7
|
require_relative "telemetry"
|
|
7
8
|
require_relative "anon_id"
|
|
@@ -12,6 +13,9 @@ module Shipeasy
|
|
|
12
13
|
module SDK
|
|
13
14
|
class FlagsClient
|
|
14
15
|
DEFAULT_BASE_URL = "https://edge.shipeasy.dev"
|
|
16
|
+
# CDN origin serving the static loader scripts (/sdk/bootstrap.js,
|
|
17
|
+
# /sdk/i18n/loader.js) — distinct from the edge API the blobs are fetched from.
|
|
18
|
+
DEFAULT_CDN_BASE = "https://cdn.shipeasy.ai"
|
|
15
19
|
|
|
16
20
|
def initialize(api_key:, base_url: nil, env: "prod", disable_telemetry: false, telemetry_url: nil, test_mode: false, private_attributes: nil, sticky_store: nil)
|
|
17
21
|
@api_key = api_key
|
|
@@ -271,6 +275,71 @@ module Shipeasy
|
|
|
271
275
|
result
|
|
272
276
|
end
|
|
273
277
|
|
|
278
|
+
# Batch-evaluate every loaded gate, config and experiment for +user+ into
|
|
279
|
+
# a bootstrap payload (+{ "flags" => ..., "configs" => ..., "experiments"
|
|
280
|
+
# => ..., "killswitches" => ... }+) keyed to match the browser SDK's
|
|
281
|
+
# window.__SE_BOOTSTRAP shape. Local overrides win. Killswitches are folded
|
|
282
|
+
# into per-gate evaluation, so the standalone +killswitches+ map is empty
|
|
283
|
+
# for this SDK. No telemetry (a batch evaluate is not a per-flag exposure).
|
|
284
|
+
def evaluate(user)
|
|
285
|
+
u = with_anon_id(user)
|
|
286
|
+
flags_blob, exps_blob, flag_ov, config_ov, exp_ov, sticky = @mutex.synchronize do
|
|
287
|
+
[@flags_blob, @exps_blob, @flag_overrides.dup, @config_overrides.dup,
|
|
288
|
+
@exp_overrides.dup, @sticky_store]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
flags = {}
|
|
292
|
+
(flags_blob&.dig("gates") || {}).each do |name, gate|
|
|
293
|
+
flags[name] = flag_ov.key?(name) ? flag_ov[name] : Eval.eval_gate(gate, u)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
configs = {}
|
|
297
|
+
(flags_blob&.dig("configs") || {}).each do |name, entry|
|
|
298
|
+
configs[name] = config_ov.key?(name) ? config_ov[name] : entry["value"]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
experiments = {}
|
|
302
|
+
(exps_blob&.dig("experiments") || {}).each do |name, exp|
|
|
303
|
+
if exp_ov.key?(name)
|
|
304
|
+
ov = exp_ov[name]
|
|
305
|
+
experiments[name] = { "inExperiment" => true, "group" => ov[:group], "params" => ov[:params] }
|
|
306
|
+
next
|
|
307
|
+
end
|
|
308
|
+
r = Eval.eval_experiment(exp, flags_blob, exps_blob, u, exp_name: name, sticky_store: sticky)
|
|
309
|
+
experiments[name] = { "inExperiment" => r.in_experiment, "group" => r.group, "params" => r.params }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
{ "flags" => flags, "configs" => configs, "experiments" => experiments, "killswitches" => {} }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Return the cross-platform SSR bootstrap <script> tag for a request:
|
|
316
|
+
# se-bootstrap.js reads its data-* attributes and hydrates
|
|
317
|
+
# window.__SE_BOOTSTRAP (and writes the anon cookie). No key is embedded.
|
|
318
|
+
def bootstrap_script_tag(user, anon_id: nil, i18n_profile: "en:prod", base_url: nil)
|
|
319
|
+
payload = evaluate(user)
|
|
320
|
+
base = cdn_base(base_url)
|
|
321
|
+
attrs = [
|
|
322
|
+
"data-se-bootstrap",
|
|
323
|
+
attr("data-flags", JSON.generate(payload["flags"])),
|
|
324
|
+
attr("data-configs", JSON.generate(payload["configs"])),
|
|
325
|
+
attr("data-experiments", JSON.generate(payload["experiments"])),
|
|
326
|
+
attr("data-killswitches", JSON.generate(payload["killswitches"])),
|
|
327
|
+
attr("data-i18n-profile", i18n_profile || "en:prod"),
|
|
328
|
+
attr("data-api-url", base),
|
|
329
|
+
]
|
|
330
|
+
attrs << attr("data-anon-id", anon_id) if anon_id && !anon_id.empty?
|
|
331
|
+
%(<script src="#{CGI.escapeHTML("#{base}/sdk/bootstrap.js")}" #{attrs.join(' ')}></script>)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Return the i18n loader <script> tag (framework-agnostic; the Rails view
|
|
335
|
+
# helper Shipeasy::I18n::ViewHelpers#i18n_script_tag is separate). The
|
|
336
|
+
# loader fetches translations for the profile using the PUBLIC client key.
|
|
337
|
+
def i18n_script_tag(client_key, profile: "en:prod", base_url: nil)
|
|
338
|
+
base = cdn_base(base_url)
|
|
339
|
+
%(<script src="#{CGI.escapeHTML("#{base}/sdk/i18n/loader.js")}" ) +
|
|
340
|
+
%(#{attr('data-key', client_key)} #{attr('data-profile', profile || 'en:prod')}></script>)
|
|
341
|
+
end
|
|
342
|
+
|
|
274
343
|
def track(user_id, event_name, props = {})
|
|
275
344
|
return if @test_mode
|
|
276
345
|
|
|
@@ -427,6 +496,14 @@ module Shipeasy
|
|
|
427
496
|
v.nil? || v == ""
|
|
428
497
|
end
|
|
429
498
|
|
|
499
|
+
def cdn_base(override)
|
|
500
|
+
(override && !override.empty? ? override : DEFAULT_CDN_BASE).chomp("/")
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def attr(name, value)
|
|
504
|
+
%(#{name}="#{CGI.escapeHTML(value.to_s)}")
|
|
505
|
+
end
|
|
506
|
+
|
|
430
507
|
def start_poll
|
|
431
508
|
@timer = Thread.new do
|
|
432
509
|
loop do
|
data/lib/shipeasy/sdk/version.rb
CHANGED