shipeasy-sdk 1.5.0 → 1.6.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/lib/shipeasy/sdk/flags_client.rb +61 -0
- data/lib/shipeasy/sdk/see.rb +284 -0
- data/lib/shipeasy/sdk/version.rb +1 -1
- data/lib/shipeasy-sdk.rb +49 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73c3a9cf2fd1a6fb0a223b9db5bcbe13cd828bfd96a4e901de545ff56d254ce7
|
|
4
|
+
data.tar.gz: '0489c3e16d16592b1301e888c82fa1d5947c21c08c74adbd9eacb6db86420138'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f2a91c5482d5eb3baf71e744116b7295ad50b19cea9a96c2ce6b6faa01467a38ca4db91f449ae773de76144156eaeec14052d14d12f3c68a3dc521fa1c6813b
|
|
7
|
+
data.tar.gz: b8e2512bbc85e404fa4528c411700bff22e7eea40f20be2d6392bd7f59166eb65464cfe17760f8b7a2f56c0eb2c7ca15a42fbc579c0937f04ce61c402824a862
|
|
@@ -6,6 +6,7 @@ require_relative "eval"
|
|
|
6
6
|
require_relative "telemetry"
|
|
7
7
|
require_relative "anon_id"
|
|
8
8
|
require_relative "sticky_store"
|
|
9
|
+
require_relative "see"
|
|
9
10
|
|
|
10
11
|
module Shipeasy
|
|
11
12
|
module SDK
|
|
@@ -15,6 +16,9 @@ module Shipeasy
|
|
|
15
16
|
def initialize(api_key:, base_url: nil, env: "prod", disable_telemetry: false, telemetry_url: nil, test_mode: false, private_attributes: nil, sticky_store: nil)
|
|
16
17
|
@api_key = api_key
|
|
17
18
|
@base_url = (base_url || DEFAULT_BASE_URL).chomp("/")
|
|
19
|
+
# Read-env tag. Used by telemetry below and stamped onto see() error
|
|
20
|
+
# events so reports are attributable to an environment.
|
|
21
|
+
@env = env
|
|
18
22
|
# Attribute names usable for targeting but stripped from every outbound
|
|
19
23
|
# /collect payload (LD/Statsig privateAttributes). The server evaluates
|
|
20
24
|
# locally so private attrs never leave for evaluation; the only egress is
|
|
@@ -55,6 +59,13 @@ module Shipeasy
|
|
|
55
59
|
# (HTTP 200, not 304). Never fired in test/offline mode. Guarded by
|
|
56
60
|
# @mutex; see on_change / notify_change.
|
|
57
61
|
@change_listeners = []
|
|
62
|
+
# see() structured error reporting. Per-process spam guard, bound here so
|
|
63
|
+
# repeated reports of the same issue collapse to one send. See see.rb.
|
|
64
|
+
@see_limiter = See::Limiter.new
|
|
65
|
+
# Register as the default client backing the module-level Shipeasy::SDK
|
|
66
|
+
# .see/.see_violation funcs (last constructed wins — the server-SDK
|
|
67
|
+
# analog of TS's shipeasy({key}) configure call).
|
|
68
|
+
Shipeasy::SDK.set_default_client(self)
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
# Build a no-network, immediately-usable client for tests. Telemetry is
|
|
@@ -313,8 +324,58 @@ module Shipeasy
|
|
|
313
324
|
end
|
|
314
325
|
end
|
|
315
326
|
|
|
327
|
+
# ---- see() structured error reporting -------------------------------
|
|
328
|
+
|
|
329
|
+
# Report a caught exception (or thrown non-exception). Fire-and-forget;
|
|
330
|
+
# never blocks or throws into the request path. Terminate with
|
|
331
|
+
# `.to(outcome)`:
|
|
332
|
+
#
|
|
333
|
+
# client.see(e).causes_the("checkout").to("use cached prices")
|
|
334
|
+
def see(problem)
|
|
335
|
+
See::Chain.new(problem, method(:dispatch_see))
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Report a non-exception problem. The name is a stable fingerprint key —
|
|
339
|
+
# put variable data in `.extras`, never in the name.
|
|
340
|
+
def see_violation(name)
|
|
341
|
+
See::Chain.new(See::Violation.new(name), method(:dispatch_see))
|
|
342
|
+
end
|
|
343
|
+
alias seeViolation see_violation
|
|
344
|
+
|
|
345
|
+
# Mark an exception as expected control flow — reports nothing. Returns a
|
|
346
|
+
# `.because(reason)` tail (with optional `.extras` for local debug only).
|
|
347
|
+
def control_flow_exception(err)
|
|
348
|
+
See::ControlFlowChain.new(err)
|
|
349
|
+
end
|
|
350
|
+
alias controlFlowException control_flow_exception
|
|
351
|
+
|
|
316
352
|
private
|
|
317
353
|
|
|
354
|
+
# Build the wire event and fire-and-forget POST it to /collect. No-op in
|
|
355
|
+
# test mode (mirrors track). Spam-guarded. Never raises into caller code.
|
|
356
|
+
def dispatch_see(built)
|
|
357
|
+
return if @test_mode
|
|
358
|
+
|
|
359
|
+
ev = See.build_event(
|
|
360
|
+
built.problem,
|
|
361
|
+
built.subject,
|
|
362
|
+
built.outcome,
|
|
363
|
+
strip_private(built.extras),
|
|
364
|
+
sdk_version: Shipeasy::SDK::VERSION,
|
|
365
|
+
env: @env,
|
|
366
|
+
)
|
|
367
|
+
return unless @see_limiter.should_send?(ev)
|
|
368
|
+
|
|
369
|
+
payload = JSON.generate({ events: [ev] })
|
|
370
|
+
Thread.new do
|
|
371
|
+
post("/collect", payload)
|
|
372
|
+
rescue => e
|
|
373
|
+
warn "[shipeasy] see() send failed: #{e.message}"
|
|
374
|
+
end
|
|
375
|
+
rescue => e
|
|
376
|
+
warn "[shipeasy] see() failed: #{e.message}"
|
|
377
|
+
end
|
|
378
|
+
|
|
318
379
|
# Drop caller-marked private attributes from an outbound props bag. Handles
|
|
319
380
|
# both string and symbol keys against the stringified private list.
|
|
320
381
|
def strip_private(props)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# see — shipeasy error. Structured error reporting for the server SDK.
|
|
2
|
+
#
|
|
3
|
+
# Mirrors `@shipeasy/sdk` (packages/ts-sdk/src/see/core.ts) and the Python
|
|
4
|
+
# reference (packages/server-sdks/sdk-python/shipeasy/_see.py). Every handled
|
|
5
|
+
# exception documents its product *consequence*, not just its stack:
|
|
6
|
+
#
|
|
7
|
+
# begin
|
|
8
|
+
# charge_card(order)
|
|
9
|
+
# rescue => e
|
|
10
|
+
# Shipeasy::SDK.see(e).causes_the("checkout").to("use the backup processor")
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Dispatch model (differs from TS, which uses a microtask): `.to(outcome)` is
|
|
14
|
+
# the terminal — it builds the wire event and fire-and-forgets the POST to
|
|
15
|
+
# /collect. `causes_the` and `extras` are chainable setters that may be called
|
|
16
|
+
# in any order *before* `.to`:
|
|
17
|
+
#
|
|
18
|
+
# client.see(e).causes_the("checkout").to("use cached prices")
|
|
19
|
+
# client.see(e).causes_the("checkout").extras({ order_id: oid }).to("use cached prices")
|
|
20
|
+
#
|
|
21
|
+
# If you don't know the consequence of an exception, don't catch it.
|
|
22
|
+
|
|
23
|
+
require "thread"
|
|
24
|
+
require "json"
|
|
25
|
+
require_relative "version"
|
|
26
|
+
|
|
27
|
+
module Shipeasy
|
|
28
|
+
module SDK
|
|
29
|
+
module See
|
|
30
|
+
# ---- Limits (mirror core.ts; kept in sync with the worker's /collect) ----
|
|
31
|
+
SEE_MAX_MESSAGE = 500
|
|
32
|
+
SEE_MAX_STACK = 8000
|
|
33
|
+
SEE_MAX_SUBJECT = 200 # used for subject, outcome, error_type
|
|
34
|
+
SEE_MAX_EXTRA_VALUE = 200
|
|
35
|
+
SEE_MAX_EXTRA_KEYS = 20
|
|
36
|
+
SEE_DEDUP_WINDOW_MS = 30_000
|
|
37
|
+
SEE_MAX_PER_PROCESS = 25
|
|
38
|
+
|
|
39
|
+
# Default consequence parts when a chain omits them.
|
|
40
|
+
DEFAULT_SUBJECT = "app".freeze
|
|
41
|
+
DEFAULT_OUTCOME = "hit an error".freeze
|
|
42
|
+
|
|
43
|
+
# Marker attribute stamped onto an exception by control_flow_exception().
|
|
44
|
+
EXPECTED_IVAR = :@__shipeasy_see_expected
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
def truncate(str, limit)
|
|
49
|
+
s = str.to_s
|
|
50
|
+
s.length <= limit ? s : s[0, limit]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Drop nil values, keep only String/Numeric(finite)/boolean, truncate
|
|
54
|
+
# string values to 200 chars, cap at 20 keys (insertion order). Returns
|
|
55
|
+
# nil if nothing is kept. Keys are stringified.
|
|
56
|
+
def sanitize_extras(extras)
|
|
57
|
+
return nil unless extras.is_a?(Hash)
|
|
58
|
+
return nil if extras.empty?
|
|
59
|
+
|
|
60
|
+
out = {}
|
|
61
|
+
extras.each do |k, v|
|
|
62
|
+
break if out.size >= SEE_MAX_EXTRA_KEYS
|
|
63
|
+
next if v.nil?
|
|
64
|
+
|
|
65
|
+
case v
|
|
66
|
+
when true, false
|
|
67
|
+
out[k.to_s] = v
|
|
68
|
+
when String
|
|
69
|
+
out[k.to_s] = truncate(v, SEE_MAX_EXTRA_VALUE)
|
|
70
|
+
when Numeric
|
|
71
|
+
# Reject NaN / Infinity (not representable in JSON).
|
|
72
|
+
next if v.respond_to?(:finite?) && !v.finite?
|
|
73
|
+
|
|
74
|
+
out[k.to_s] = v
|
|
75
|
+
else
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
out.empty? ? nil : out
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Best-effort stamp marking an exception as expected control flow.
|
|
83
|
+
def mark_expected(err, because, extras = nil)
|
|
84
|
+
mark = { "because" => because.to_s }
|
|
85
|
+
clean = sanitize_extras(extras)
|
|
86
|
+
mark["extras"] = clean if clean
|
|
87
|
+
err.instance_variable_set(EXPECTED_IVAR, mark)
|
|
88
|
+
rescue StandardError
|
|
89
|
+
# Frozen / builtin objects that reject ivars: best effort only.
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def expected?(err)
|
|
94
|
+
err.instance_variable_defined?(EXPECTED_IVAR) &&
|
|
95
|
+
!err.instance_variable_get(EXPECTED_IVAR).nil?
|
|
96
|
+
rescue StandardError
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A non-exception problem. The name is a stable fingerprint key — put
|
|
101
|
+
# variable data in `.extras`, never in the name.
|
|
102
|
+
class Violation
|
|
103
|
+
attr_reader :name
|
|
104
|
+
|
|
105
|
+
def initialize(name)
|
|
106
|
+
@name = name.to_s
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ---- Wire event construction ----
|
|
111
|
+
|
|
112
|
+
# Build the type:"error" event accepted by POST /collect.
|
|
113
|
+
def build_event(problem, subject, outcome, extras, sdk_version:, env:)
|
|
114
|
+
stack = nil
|
|
115
|
+
|
|
116
|
+
if problem.is_a?(Violation)
|
|
117
|
+
error_type = problem.name
|
|
118
|
+
message = problem.name
|
|
119
|
+
kind = "violation"
|
|
120
|
+
elsif problem.is_a?(Exception)
|
|
121
|
+
error_type = problem.class.name || "Error"
|
|
122
|
+
message = (problem.message.to_s.empty? ? error_type : problem.message)
|
|
123
|
+
bt = problem.backtrace
|
|
124
|
+
stack = bt.join("\n") if bt && !bt.empty?
|
|
125
|
+
kind = "caught"
|
|
126
|
+
else
|
|
127
|
+
error_type = "Error"
|
|
128
|
+
message = problem.to_s
|
|
129
|
+
kind = "caught"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
ev = {
|
|
133
|
+
"type" => "error",
|
|
134
|
+
"kind" => kind,
|
|
135
|
+
"error_type" => truncate(error_type, SEE_MAX_SUBJECT),
|
|
136
|
+
"message" => truncate(message, SEE_MAX_MESSAGE),
|
|
137
|
+
"subject" => truncate(subject, SEE_MAX_SUBJECT),
|
|
138
|
+
"outcome" => truncate(outcome, SEE_MAX_SUBJECT),
|
|
139
|
+
"side" => "server",
|
|
140
|
+
"sdk_version" => sdk_version,
|
|
141
|
+
"ts" => (Time.now.to_f * 1000).to_i,
|
|
142
|
+
}
|
|
143
|
+
ev["stack"] = truncate(stack, SEE_MAX_STACK) if stack
|
|
144
|
+
clean = sanitize_extras(extras)
|
|
145
|
+
ev["extras"] = clean if clean
|
|
146
|
+
ev["env"] = env if env && !env.to_s.empty?
|
|
147
|
+
ev
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ---- Spam limiter (mirror SeeLimiter) ----
|
|
151
|
+
|
|
152
|
+
# Per-process spam guard: identical events within 30s collapse to one
|
|
153
|
+
# send; a hard cap bounds total sends. Thread-safe. The worker dedupes by
|
|
154
|
+
# fingerprint anyway — this only bounds network chatter from a hot loop.
|
|
155
|
+
class Limiter
|
|
156
|
+
def initialize(max_per_process: SEE_MAX_PER_PROCESS, dedup_window_ms: SEE_DEDUP_WINDOW_MS)
|
|
157
|
+
@max = max_per_process
|
|
158
|
+
@window = dedup_window_ms
|
|
159
|
+
@last = {}
|
|
160
|
+
@sent = 0
|
|
161
|
+
@mutex = Mutex.new
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def should_send?(ev)
|
|
165
|
+
@mutex.synchronize do
|
|
166
|
+
return false if @sent >= @max
|
|
167
|
+
|
|
168
|
+
key = [
|
|
169
|
+
ev["kind"],
|
|
170
|
+
ev["error_type"],
|
|
171
|
+
ev["message"].to_s[0, 200],
|
|
172
|
+
See.top_stack_line(ev["stack"]),
|
|
173
|
+
].join("|")
|
|
174
|
+
now = (Time.now.to_f * 1000).to_i
|
|
175
|
+
prev = @last[key]
|
|
176
|
+
return false if prev && (now - prev) < @window
|
|
177
|
+
|
|
178
|
+
@last[key] = now
|
|
179
|
+
@sent += 1
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def top_stack_line(stack)
|
|
186
|
+
return "" if stack.nil? || stack.empty?
|
|
187
|
+
|
|
188
|
+
stack.each_line do |line|
|
|
189
|
+
s = line.strip
|
|
190
|
+
return s[0, 200] if s.start_with?("File ") || s.start_with?("at ") || s.include?("line ") || s.include?(":in ")
|
|
191
|
+
end
|
|
192
|
+
""
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ---- Fluent chains ----
|
|
196
|
+
|
|
197
|
+
# Accumulates consequence + extras; `.to(outcome)` dispatches once.
|
|
198
|
+
class Chain
|
|
199
|
+
def initialize(problem, dispatch)
|
|
200
|
+
@problem = problem
|
|
201
|
+
@dispatch = dispatch
|
|
202
|
+
@subject = nil
|
|
203
|
+
@outcome = nil
|
|
204
|
+
@extras = nil
|
|
205
|
+
@done = false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def causes_the(subject)
|
|
209
|
+
@subject = subject.to_s
|
|
210
|
+
self
|
|
211
|
+
end
|
|
212
|
+
alias causesThe causes_the
|
|
213
|
+
|
|
214
|
+
def extras(extras)
|
|
215
|
+
if extras.is_a?(Hash) && !extras.empty?
|
|
216
|
+
@extras = (@extras || {}).merge(extras)
|
|
217
|
+
end
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Terminal: build the event and fire-and-forget the report. Idempotent.
|
|
222
|
+
def to(outcome)
|
|
223
|
+
return if @done
|
|
224
|
+
|
|
225
|
+
@done = true
|
|
226
|
+
@outcome = outcome.to_s
|
|
227
|
+
begin
|
|
228
|
+
@dispatch.call(
|
|
229
|
+
Built.new(@problem, @subject || DEFAULT_SUBJECT, @outcome.empty? ? DEFAULT_OUTCOME : @outcome, @extras)
|
|
230
|
+
)
|
|
231
|
+
rescue StandardError
|
|
232
|
+
# Reporting must never raise into caller code.
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Plain carrier of a finalized chain handed to the client dispatcher.
|
|
239
|
+
Built = Struct.new(:problem, :subject, :outcome, :extras)
|
|
240
|
+
|
|
241
|
+
# `control_flow_exception(e).because("because ...")` — marks the exception
|
|
242
|
+
# expected and reports NOTHING. `.extras` is stored for local debugging
|
|
243
|
+
# only (an expected exception is never transmitted).
|
|
244
|
+
class ControlFlowChain
|
|
245
|
+
def initialize(err)
|
|
246
|
+
@err = err
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def because(reason)
|
|
250
|
+
See.mark_expected(@err, reason)
|
|
251
|
+
ControlFlowTail.new(@err, reason)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
class ControlFlowTail
|
|
256
|
+
def initialize(err, reason)
|
|
257
|
+
@err = err
|
|
258
|
+
@reason = reason
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def extras(extras)
|
|
262
|
+
See.mark_expected(@err, @reason, extras)
|
|
263
|
+
self
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# A no-op chain returned by the module-level see() when no client exists.
|
|
268
|
+
class NullChain
|
|
269
|
+
def causes_the(_subject)
|
|
270
|
+
self
|
|
271
|
+
end
|
|
272
|
+
alias causesThe causes_the
|
|
273
|
+
|
|
274
|
+
def extras(_extras)
|
|
275
|
+
self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def to(_outcome)
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
data/lib/shipeasy/sdk/version.rb
CHANGED
data/lib/shipeasy-sdk.rb
CHANGED
|
@@ -25,5 +25,54 @@ module Shipeasy
|
|
|
25
25
|
def self.new_client(api_key: Shipeasy.config.api_key, base_url: Shipeasy.config.base_url)
|
|
26
26
|
FlagsClient.new(api_key: api_key, base_url: base_url)
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
# ---- see() module-level facade --------------------------------------
|
|
30
|
+
#
|
|
31
|
+
# Backed by a default client, registered when a FlagsClient is constructed
|
|
32
|
+
# (last constructed wins). Mirrors the package-level see() in the TS/Python
|
|
33
|
+
# SDKs so callers can `Shipeasy::SDK.see(e).causes_the(...).to(...)` without
|
|
34
|
+
# threading a client reference through every call site. A call before any
|
|
35
|
+
# client exists warns and returns a no-op chain (NEVER raises).
|
|
36
|
+
|
|
37
|
+
@see_default_client = nil
|
|
38
|
+
@see_default_mutex = Mutex.new
|
|
39
|
+
|
|
40
|
+
# Register the client backing the module-level see() funcs. Called
|
|
41
|
+
# automatically from FlagsClient#initialize; also exposed for explicit use.
|
|
42
|
+
def self.set_default_client(client)
|
|
43
|
+
@see_default_mutex.synchronize { @see_default_client = client }
|
|
44
|
+
client
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.default_client
|
|
48
|
+
@see_default_mutex.synchronize { @see_default_client }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Report a caught exception via the default client. Use client.see to
|
|
52
|
+
# target a specific client.
|
|
53
|
+
def self.see(problem)
|
|
54
|
+
client = default_client
|
|
55
|
+
if client.nil?
|
|
56
|
+
warn "[shipeasy] see() called before a client was created — error dropped"
|
|
57
|
+
return See::NullChain.new
|
|
58
|
+
end
|
|
59
|
+
client.see(problem)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Report a non-exception problem via the default client.
|
|
63
|
+
def self.see_violation(name)
|
|
64
|
+
client = default_client
|
|
65
|
+
if client.nil?
|
|
66
|
+
warn "[shipeasy] see_violation() called before a client was created — error dropped"
|
|
67
|
+
return See::NullChain.new
|
|
68
|
+
end
|
|
69
|
+
client.see_violation(name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Mark an exception as expected control flow (reports nothing). Works
|
|
73
|
+
# without a client — it only stamps the exception object.
|
|
74
|
+
def self.control_flow_exception(err)
|
|
75
|
+
See::ControlFlowChain.new(err)
|
|
76
|
+
end
|
|
28
77
|
end
|
|
29
78
|
end
|
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: 1.
|
|
4
|
+
version: 1.6.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-06-
|
|
11
|
+
date: 2026-06-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -76,6 +76,7 @@ files:
|
|
|
76
76
|
- lib/shipeasy/sdk/openfeature.rb
|
|
77
77
|
- lib/shipeasy/sdk/rack_middleware.rb
|
|
78
78
|
- lib/shipeasy/sdk/railtie.rb
|
|
79
|
+
- lib/shipeasy/sdk/see.rb
|
|
79
80
|
- lib/shipeasy/sdk/sticky_store.rb
|
|
80
81
|
- lib/shipeasy/sdk/telemetry.rb
|
|
81
82
|
- lib/shipeasy/sdk/version.rb
|