shipeasy-sdk 3.1.1 → 3.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.
- checksums.yaml +4 -4
- data/docs/skill/SKILL.md +3 -3
- data/lib/shipeasy/config.rb +17 -0
- data/lib/shipeasy/engine.rb +7 -2
- data/lib/shipeasy/i18n/view_helpers.rb +4 -0
- data/lib/shipeasy/sdk/env.rb +8 -0
- data/lib/shipeasy/sdk/eval.rb +56 -6
- data/lib/shipeasy/sdk/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60cefd35bba21f86ef6b77da83d0c918744721d9c564066b12e8d2b95dba8740
|
|
4
|
+
data.tar.gz: 22b03ac85fbd29bba861770120c2a78bbcbc5069e0e7af0cbe8b1c33b8ebee88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 226eccc8d981ce982ed132eb697c39e37cde9f9d62d9f84970cea6119601696383520bc1f03606abd58b81574be31d26962aedf2e000acfe45e1ac7bdb2c5f2c
|
|
7
|
+
data.tar.gz: 1e213dba4152299e0e0ad0d4480f02ca55c108d3fc7e5b70c966612799972a8b450fb24e5a74f4524e719d007994211fa903e6c5b76706012c9ba57f786b547e
|
data/docs/skill/SKILL.md
CHANGED
|
@@ -60,9 +60,9 @@ flags.get_killswitch("payments") # true = killed; optional switch_ke
|
|
|
60
60
|
|
|
61
61
|
# Ask the UNIVERSE, not the experiment. Returns an Assignment (never raises):
|
|
62
62
|
# .name / .group → nil when not enrolled · .enrolled? → group non-nil
|
|
63
|
-
# .get(field, fallback = nil) → variant override ?? universe default ?? fallback
|
|
64
|
-
assignment = flags.universe("checkout").assign #
|
|
65
|
-
render_cta(assignment.get("label", "Buy now"))
|
|
63
|
+
# .get(field, fallback = nil, exposure: true) → variant override ?? universe default ?? fallback
|
|
64
|
+
assignment = flags.universe("checkout").assign # side-effect free; exposure fires on first get read
|
|
65
|
+
render_cta(assignment.get("label", "Buy now")) # first read logs one deduped exposure (exposure: false to peek)
|
|
66
66
|
|
|
67
67
|
flags.track("purchase", { revenue: 49 }) # conversion / metric event
|
|
68
68
|
```
|
data/lib/shipeasy/config.rb
CHANGED
|
@@ -85,6 +85,13 @@ module Shipeasy
|
|
|
85
85
|
:cdn_base_url, :loader_url,
|
|
86
86
|
:manifest_cache_ttl, :label_file_cache_ttl, :http_timeout
|
|
87
87
|
|
|
88
|
+
# When true, `i18n_t` renders the translation KEY verbatim instead of
|
|
89
|
+
# resolving its value, so tests/snapshots assert against stable data instead
|
|
90
|
+
# of copy that changes when a translation is edited. nil (default) ⇒
|
|
91
|
+
# env-derived: on when the native env is "test" (RAILS_ENV / RACK_ENV /
|
|
92
|
+
# SHIPEASY_ENV / APP_ENV), off otherwise. Set true/false to override.
|
|
93
|
+
attr_accessor :render_keys_only
|
|
94
|
+
|
|
88
95
|
def initialize
|
|
89
96
|
@base_url = "https://api.shipeasy.ai"
|
|
90
97
|
@attributes = nil
|
|
@@ -108,6 +115,16 @@ module Shipeasy
|
|
|
108
115
|
@manifest_cache_ttl = 60
|
|
109
116
|
@label_file_cache_ttl = 3600
|
|
110
117
|
@http_timeout = 1
|
|
118
|
+
# nil ⇒ env-derived (on under RAILS_ENV/…=="test"); true/false overrides.
|
|
119
|
+
@render_keys_only = nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Resolve the effective render_keys_only decision: an explicit true/false set
|
|
123
|
+
# in the configure block wins; otherwise default to env==test.
|
|
124
|
+
def render_keys_only?
|
|
125
|
+
return @render_keys_only unless @render_keys_only.nil?
|
|
126
|
+
|
|
127
|
+
Shipeasy::SDK::Env.is_test_env
|
|
111
128
|
end
|
|
112
129
|
end
|
|
113
130
|
|
data/lib/shipeasy/engine.rb
CHANGED
|
@@ -350,8 +350,13 @@ module Shipeasy
|
|
|
350
350
|
candidates.each do |name, exp|
|
|
351
351
|
result = eval_experiment(name, exp, u, flags_blob, exps_blob)
|
|
352
352
|
next unless result.in_experiment
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
group = result.group
|
|
354
|
+
# On-read exposure (spec step 7): defer the single exposure to the
|
|
355
|
+
# first param read via the callback, instead of firing it here at
|
|
356
|
+
# assign time.
|
|
357
|
+
landed = Eval::Assignment.new(name, group, result.params || {}, lambda {
|
|
358
|
+
post_exposure(u, name, group)
|
|
359
|
+
})
|
|
355
360
|
break
|
|
356
361
|
# not enrolled: try the next candidate — under pooling only one slice
|
|
357
362
|
# can match, so the loop lands on the winner (or falls through).
|
|
@@ -34,6 +34,10 @@ module Shipeasy
|
|
|
34
34
|
|
|
35
35
|
def i18n_t(key, variables = {}, profile: nil, chunk: nil)
|
|
36
36
|
config = Shipeasy.config
|
|
37
|
+
# render_keys_only (default: env==test): return the key verbatim,
|
|
38
|
+
# skipping value resolution + interpolation so tests assert stable data.
|
|
39
|
+
return key if config.render_keys_only?
|
|
40
|
+
|
|
37
41
|
label_file = Shipeasy::I18n::LabelFetcher.new.fetch(
|
|
38
42
|
profile: profile || config.profile,
|
|
39
43
|
chunk: chunk || config.default_chunk,
|
data/lib/shipeasy/sdk/env.rb
CHANGED
|
@@ -38,6 +38,14 @@ module Shipeasy
|
|
|
38
38
|
(configured_env || "prod").to_s.strip.downcase == "prod"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# True when the host runtime looks like a test run — the first present
|
|
42
|
+
# native env var (SHIPEASY_ENV / RAILS_ENV / RACK_ENV / APP_ENV) is exactly
|
|
43
|
+
# "test" (what Rails/RSpec set). Used to default i18n render_keys_only on
|
|
44
|
+
# under test. No native var present ⇒ not test.
|
|
45
|
+
def is_test_env
|
|
46
|
+
read_native_env == "test"
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
# Read the first present native env var (lowercased, trimmed), or nil when
|
|
42
50
|
# none of them is set to a non-empty value.
|
|
43
51
|
def read_native_env
|
data/lib/shipeasy/sdk/eval.rb
CHANGED
|
@@ -125,6 +125,28 @@ module Shipeasy
|
|
|
125
125
|
# universe assign() path share ONE classify. The added steps are all guarded
|
|
126
126
|
# by presence checks, so a legacy blob (no hashVersion/pool/reserved) behaves
|
|
127
127
|
# exactly as before.
|
|
128
|
+
# Resolve a forced override group for +uid+ (spec step 1): ID overrides
|
|
129
|
+
# (tier 1) beat cohort/GK overrides (tier 2); within cohort overrides the
|
|
130
|
+
# first (pre-sorted by priority) gate that passes wins. Returns the forced
|
|
131
|
+
# group name or nil. The caller applies eligibility + group-existence
|
|
132
|
+
# (forced-but-gated). Mirrors @shipeasy/core resolveForcedGroup.
|
|
133
|
+
def self.resolve_forced_group(exp, uid, flags_blob, user)
|
|
134
|
+
id_overrides = exp["idOverrides"] || exp[:idOverrides]
|
|
135
|
+
if id_overrides
|
|
136
|
+
by_id = id_overrides[uid]
|
|
137
|
+
return by_id if by_id && !by_id.to_s.empty?
|
|
138
|
+
end
|
|
139
|
+
cohort_overrides = exp["cohortOverrides"] || exp[:cohortOverrides]
|
|
140
|
+
if cohort_overrides
|
|
141
|
+
cohort_overrides.each do |co|
|
|
142
|
+
gname = co["gate"] || co[:gate]
|
|
143
|
+
gate = flags_blob&.dig("gates", gname)
|
|
144
|
+
return (co["group"] || co[:group]) if gate && eval_gate(gate, user)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
128
150
|
def self.eval_experiment(exp, flags_blob, exps_blob, user, exp_name: nil, sticky_store: nil)
|
|
129
151
|
universe_name = exp && exp["universe"]
|
|
130
152
|
universe = exps_blob&.dig("universes", universe_name)
|
|
@@ -172,6 +194,21 @@ module Shipeasy
|
|
|
172
194
|
groups = exp["groups"] || []
|
|
173
195
|
salt8 = (salt || "")[0, 8]
|
|
174
196
|
|
|
197
|
+
# Durable overrides (spec step 1, forced-but-gated). Reached only after the
|
|
198
|
+
# unit passes targeting and is not held out, so an override may now pin the
|
|
199
|
+
# group — bypassing allocation + the weighted pick but NOT the gates above.
|
|
200
|
+
# ID overrides (tier 1) beat cohort/GK overrides (tier 2); a forced group
|
|
201
|
+
# that no longer exists falls through to normal allocation. No-op when
|
|
202
|
+
# unconfigured, so v1/v2 stay byte-identical. Mirrors @shipeasy/core.
|
|
203
|
+
forced = resolve_forced_group(exp, uid, flags_blob, user)
|
|
204
|
+
if forced
|
|
205
|
+
g = groups.find { |x| x["name"] == forced }
|
|
206
|
+
if g
|
|
207
|
+
sticky_store.set(uid, exp_name, { "g" => forced, "s" => salt8 }) if sticky_store && exp_name
|
|
208
|
+
return as_group.call(g)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
175
212
|
# Sticky short-circuit: an enrolled unit whose stored salt prefix still
|
|
176
213
|
# matches skips allocation and returns the stored group. If the stored
|
|
177
214
|
# group no longer exists, fall through to re-bucket + overwrite.
|
|
@@ -232,14 +269,19 @@ module Shipeasy
|
|
|
232
269
|
attr_reader :group
|
|
233
270
|
|
|
234
271
|
# +params+ is already merged (universeDefaults ⊕ variantOverride) when
|
|
235
|
-
# enrolled; defaults-only (or {}) when not.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
@
|
|
272
|
+
# enrolled; defaults-only (or {}) when not. +on_expose+ fires the single
|
|
273
|
+
# exposure the first time an enrolled param is read (nil when not
|
|
274
|
+
# enrolled — nothing to expose); deduped downstream.
|
|
275
|
+
def initialize(name, group, params, on_expose = nil)
|
|
276
|
+
@name = name
|
|
277
|
+
@group = group
|
|
278
|
+
@params = params || {}
|
|
279
|
+
@on_expose = on_expose
|
|
280
|
+
@exposed = false
|
|
240
281
|
end
|
|
241
282
|
|
|
242
283
|
# True iff the unit is enrolled in an experiment in this universe.
|
|
284
|
+
# Reading it does NOT log an exposure (only +get+ of a param does).
|
|
243
285
|
def enrolled?
|
|
244
286
|
!@group.nil?
|
|
245
287
|
end
|
|
@@ -248,7 +290,15 @@ module Shipeasy
|
|
|
248
290
|
# universe default, else +fallback+. Works even when not enrolled (the
|
|
249
291
|
# variant layer is absent, so you get universeDefault ?? fallback).
|
|
250
292
|
# Looks up both string and symbol keys.
|
|
251
|
-
|
|
293
|
+
#
|
|
294
|
+
# Exposure is logged **on read** (spec step 7): the first enrolled read
|
|
295
|
+
# fires the single exposure; pass +exposure: false+ to read without
|
|
296
|
+
# logging (peek).
|
|
297
|
+
def get(field, fallback = nil, exposure: true)
|
|
298
|
+
if exposure && !@exposed && @on_expose
|
|
299
|
+
@exposed = true
|
|
300
|
+
@on_expose.call
|
|
301
|
+
end
|
|
252
302
|
if @params.key?(field)
|
|
253
303
|
@params[field]
|
|
254
304
|
elsif field.respond_to?(:to_s) && @params.key?(field.to_s)
|
data/lib/shipeasy/sdk/version.rb
CHANGED
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: 3.
|
|
4
|
+
version: 3.3.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-07-
|
|
11
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|