shipeasy-sdk 3.5.0 → 3.5.1
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/lib/shipeasy/sdk/eval.rb +92 -0
- 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: 936c7071cc2d4302c9523145eaa5ee80b184167bea8047ee7be9f539d2a3538e
|
|
4
|
+
data.tar.gz: ae302e9e648ed16fcf4687ad90668779434a9fda602266e0a4d7fc6a319bda81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eab22b5a957b8be557fd605f3e19024872217179b43d27d1356caec5f1c6829f2049a6ca4663bf53f58ff31dfc92bdacdbf183655b160fc653aa74d916d81304
|
|
7
|
+
data.tar.gz: 3a79b37580852b152b783dc3265232ebd123aac3176ea82b8d5a3acdd3c1285ff2163d2b682180f20005ccf0d702d862b2c7bf1cc5c6a4dcc3ac739613d527ff
|
data/lib/shipeasy/sdk/eval.rb
CHANGED
|
@@ -70,10 +70,102 @@ module Shipeasy
|
|
|
70
70
|
user["user_id"] || user[:user_id] || user["anonymous_id"] || user[:anonymous_id]
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
def self.clamp_pct(n)
|
|
74
|
+
return 0 if n < 0
|
|
75
|
+
return 10000 if n > 10000
|
|
76
|
+
n
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Effective rollout % for a stack entry at time +now+ (epoch ms). A
|
|
80
|
+
# condition with no explicit rolloutPct defaults to 100% (match => pass); a
|
|
81
|
+
# rollout to 0%. A ramp linearly interpolates from=>to over
|
|
82
|
+
# [startAt, startAt + durationMs] via truncating-toward-zero division — the
|
|
83
|
+
# cross-SDK contract (experiment-platform/04-evaluation.md). Mirrors core's
|
|
84
|
+
# effectivePct.
|
|
85
|
+
def self.effective_pct(entry, now)
|
|
86
|
+
type = entry["type"] || entry[:type]
|
|
87
|
+
raw = entry["rolloutPct"] || entry[:rolloutPct]
|
|
88
|
+
base = raw.nil? ? (type == "condition" ? 10000 : 0) : raw
|
|
89
|
+
ramp = entry["ramp"] || entry[:ramp]
|
|
90
|
+
return base unless ramp
|
|
91
|
+
|
|
92
|
+
from = ramp["from"] || ramp[:from]
|
|
93
|
+
to = ramp["to"] || ramp[:to]
|
|
94
|
+
start_at = ramp["startAt"] || ramp[:startAt]
|
|
95
|
+
duration = ramp["durationMs"] || ramp[:durationMs]
|
|
96
|
+
return from if now <= start_at
|
|
97
|
+
return to if now >= start_at + duration
|
|
98
|
+
|
|
99
|
+
delta = to - from # signed
|
|
100
|
+
elapsed = now - start_at
|
|
101
|
+
# .to_i on the float quotient truncates toward zero (works for a negative
|
|
102
|
+
# ramp-down delta, unlike Ruby's floor-division integer `/`).
|
|
103
|
+
clamp_pct(from + (delta * elapsed).fdiv(duration).to_i)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Hash the caller into [0, 10000) and test against +pct+. No-unit contract
|
|
107
|
+
# (experiment-platform/18): a fully-rolled bucket is on for everyone without
|
|
108
|
+
# a unit id; a fractional one needs a stable unit, so it is off. Mirrors
|
|
109
|
+
# core's bucketHit.
|
|
110
|
+
def self.bucket_hit(pct, uid, salt)
|
|
111
|
+
return false if pct <= 0
|
|
112
|
+
return pct >= 10000 if uid.nil? || uid.to_s.empty?
|
|
113
|
+
return true if pct >= 10000
|
|
114
|
+
murmur3("#{salt}:#{uid}") % 10000 < pct
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Evaluate one gatekeeper stack entry. A `condition` gates on its rules
|
|
118
|
+
# (pass: "all" | "any") then buckets at its own rollout % (default 100%); a
|
|
119
|
+
# `rollout` buckets everyone who reached it (default 0%). A condition's
|
|
120
|
+
# default bucketing salt is its own id so each step buckets independently;
|
|
121
|
+
# a rollout falls back to the gate salt so existing entries don't re-bucket.
|
|
122
|
+
# Mirrors core's evalStackEntry.
|
|
123
|
+
def self.eval_stack_entry(entry, user, fallback_salt, now)
|
|
124
|
+
type = entry["type"] || entry[:type]
|
|
125
|
+
bucket_by = entry["bucketBy"] || entry[:bucketBy]
|
|
126
|
+
entry_salt = entry["salt"] || entry[:salt]
|
|
127
|
+
|
|
128
|
+
if type == "condition"
|
|
129
|
+
rules = entry["rules"] || entry[:rules] || []
|
|
130
|
+
return false if rules.empty?
|
|
131
|
+
|
|
132
|
+
mode = entry["pass"] || entry[:pass] || "all"
|
|
133
|
+
matched =
|
|
134
|
+
if mode == "any"
|
|
135
|
+
rules.any? { |r| match_rule(r, user) }
|
|
136
|
+
else
|
|
137
|
+
rules.all? { |r| match_rule(r, user) }
|
|
138
|
+
end
|
|
139
|
+
return false unless matched
|
|
140
|
+
|
|
141
|
+
salt = (entry_salt && !entry_salt.to_s.empty?) ? entry_salt : (entry["id"] || entry[:id] || fallback_salt)
|
|
142
|
+
bucket_hit(effective_pct(entry, now), pick_identifier(user, bucket_by), salt)
|
|
143
|
+
else
|
|
144
|
+
salt = (entry_salt && !entry_salt.to_s.empty?) ? entry_salt : fallback_salt
|
|
145
|
+
bucket_hit(effective_pct(entry, now), pick_identifier(user, bucket_by), salt)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
73
149
|
def self.eval_gate(gate, user)
|
|
74
150
|
return false if enabled?(gate["killswitch"])
|
|
75
151
|
return false unless enabled?(gate["enabled"])
|
|
76
152
|
|
|
153
|
+
# Modern gatekeepers ship an ordered `stack`; evaluate it top-to-bottom and
|
|
154
|
+
# pass on the first entry whose rules match AND whose bucket hits. This is
|
|
155
|
+
# the canonical model — the flat `rules`/`rolloutPct` below are a lossy
|
|
156
|
+
# approximation (a whitelist condition at 100% collapses to `rolloutPct: 0`
|
|
157
|
+
# once the public rollout is 0%, which the flat path would wrongly read as
|
|
158
|
+
# "never"). Mirrors @shipeasy/core evalGatekeeper — keep the two in sync.
|
|
159
|
+
stack = gate["stack"] || gate[:stack]
|
|
160
|
+
if stack.is_a?(Array) && !stack.empty?
|
|
161
|
+
now = (Time.now.to_f * 1000).to_i
|
|
162
|
+
gate_salt = gate["salt"] || gate[:salt]
|
|
163
|
+
stack.each do |entry|
|
|
164
|
+
return true if eval_stack_entry(entry, user, gate_salt, now)
|
|
165
|
+
end
|
|
166
|
+
return false
|
|
167
|
+
end
|
|
168
|
+
|
|
77
169
|
(gate["rules"] || []).each do |rule|
|
|
78
170
|
return false unless match_rule(rule, user)
|
|
79
171
|
end
|
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.5.
|
|
4
|
+
version: 3.5.1
|
|
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-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|