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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73c3a9cf2fd1a6fb0a223b9db5bcbe13cd828bfd96a4e901de545ff56d254ce7
4
- data.tar.gz: '0489c3e16d16592b1301e888c82fa1d5947c21c08c74adbd9eacb6db86420138'
3
+ metadata.gz: afdce684e3c6a83b220b7dfaa1d7db249284e3bb4c4ebfe3c9b27c54da0d801b
4
+ data.tar.gz: ecdfdf8aed19f1b5f18fe874ce5b13064bb61abe7bf7f1c47a76b08d9ab99bce
5
5
  SHA512:
6
- metadata.gz: 9f2a91c5482d5eb3baf71e744116b7295ad50b19cea9a96c2ce6b6faa01467a38ca4db91f449ae773de76144156eaeec14052d14d12f3c68a3dc521fa1c6813b
7
- data.tar.gz: b8e2512bbc85e404fa4528c411700bff22e7eea40f20be2d6392bd7f59166eb65464cfe17760f8b7a2f56c0eb2c7ca15a42fbc579c0937f04ce61c402824a862
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
@@ -1,5 +1,5 @@
1
1
  module Shipeasy
2
2
  module SDK
3
- VERSION = "1.6.0"
3
+ VERSION = "1.7.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shipeasy-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shipeasy, Inc.