shipeasy-sdk 3.1.1 → 3.4.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 +9 -4
- data/lib/shipeasy/config.rb +28 -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/rack_middleware.rb +4 -1
- data/lib/shipeasy/sdk/see.rb +104 -8
- data/lib/shipeasy/sdk/version.rb +1 -1
- data/lib/shipeasy-sdk.rb +29 -0
- 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: c1b1f478bea3279f979e85e04476381f719d3db27aa29ffb23447768ceefc560
|
|
4
|
+
data.tar.gz: c0d4f8f1ca49a6a1b34ac3b1198a3788dc115f640eb5845fb388a66175e1384c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94790b0a88b1524f35faf706245339db83908565b5adec3a7f8b9f5775d476cb703ce00ac55a697d79a343a1abe0c94145c2fd693538c1aecbdab81385525148
|
|
7
|
+
data.tar.gz: f35b460dd560167674a1a278c1e76600fd59040016ad06b3129ee77c5dd95d68d57d0c7695ca06dee7f7870f15489655ee20ca644b6d1a24d416fdf280c2daf7
|
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
|
```
|
|
@@ -126,12 +126,17 @@ rescue => e
|
|
|
126
126
|
end
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
Attach context with `.extras({...})` before `.to`, or inline as
|
|
130
|
+
`.to(outcome, {...})`. To attach it from anywhere without threading it into the
|
|
131
|
+
rescue, buffer it earlier with `Shipeasy.add_extras(order_id: id)` — it merges
|
|
132
|
+
into every see() report later in the same (fiber-local, per-request) scope.
|
|
133
|
+
|
|
129
134
|
`Shipeasy.see_violation(name)` for non-exception problems;
|
|
130
135
|
`Shipeasy.control_flow_exception(e).because(...)` marks expected control flow
|
|
131
136
|
(reports nothing).
|
|
132
137
|
|
|
133
138
|
→ More: `pages/error-reporting.md` · snippets `snippets/ops/see.md`
|
|
134
|
-
(`.extras`, violations, control-flow exceptions).
|
|
139
|
+
(`.extras`, `add_extras`, violations, control-flow exceptions).
|
|
135
140
|
|
|
136
141
|
## i18n (Rails)
|
|
137
142
|
|
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
|
|
|
@@ -309,6 +326,17 @@ module Shipeasy
|
|
|
309
326
|
Shipeasy::SDK.control_flow_exception(err)
|
|
310
327
|
end
|
|
311
328
|
|
|
329
|
+
# Ambient per-request see() extras — attach context from anywhere that
|
|
330
|
+
# merges into every see() report firing later in the same request, so you
|
|
331
|
+
# never thread it into the rescue block. See Shipeasy::SDK.add_extras.
|
|
332
|
+
def add_extras(extras = nil, **kwargs)
|
|
333
|
+
Shipeasy::SDK.add_extras(extras, **kwargs)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def clear_extras
|
|
337
|
+
Shipeasy::SDK.clear_extras
|
|
338
|
+
end
|
|
339
|
+
|
|
312
340
|
# Replace the registered global engine + attributes transform (used by the
|
|
313
341
|
# configure_for_* siblings — unlike configure, they replace so a test suite
|
|
314
342
|
# can reconfigure between cases). Returns the engine.
|
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)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "anon_id"
|
|
2
|
+
require_relative "see"
|
|
2
3
|
|
|
3
4
|
module Shipeasy
|
|
4
5
|
module SDK
|
|
@@ -32,8 +33,10 @@ module Shipeasy
|
|
|
32
33
|
begin
|
|
33
34
|
status, headers, body = @app.call(env)
|
|
34
35
|
ensure
|
|
35
|
-
# Don't leak the id
|
|
36
|
+
# Don't leak the id — or any ambient see() extras — onto the next
|
|
37
|
+
# request handled by this thread.
|
|
36
38
|
AnonId.current = nil
|
|
39
|
+
See::Context.clear
|
|
37
40
|
end
|
|
38
41
|
set_cookie!(headers, id, env) if minted
|
|
39
42
|
[status, headers, body]
|
data/lib/shipeasy/sdk/see.rb
CHANGED
|
@@ -12,11 +12,22 @@
|
|
|
12
12
|
#
|
|
13
13
|
# Dispatch model (differs from TS, which uses a microtask): `.to(outcome)` is
|
|
14
14
|
# the terminal — it builds the wire event and fire-and-forgets the POST to
|
|
15
|
-
# /collect. `causes_the` and `extras` are chainable setters
|
|
16
|
-
#
|
|
15
|
+
# /collect. `causes_the` and `extras` are chainable setters called *before*
|
|
16
|
+
# `.to`; `.to` also accepts the extras inline as a second arg, so there is no
|
|
17
|
+
# ordering trap to remember:
|
|
17
18
|
#
|
|
18
19
|
# client.see(e).causes_the("checkout").to("use cached prices")
|
|
19
20
|
# client.see(e).causes_the("checkout").extras({ order_id: oid }).to("use cached prices")
|
|
21
|
+
# client.see(e).causes_the("checkout").to("use cached prices", { order_id: oid })
|
|
22
|
+
#
|
|
23
|
+
# `.extras` chained AFTER `.to` is ignored with a warning (the report already
|
|
24
|
+
# went out) — it never raises into the rescue block.
|
|
25
|
+
#
|
|
26
|
+
# To attach context from anywhere in a request without threading it into the
|
|
27
|
+
# rescue block, use the ambient buffer (see `Context` below):
|
|
28
|
+
# `Shipeasy.add_extras(order_id: oid)` merges into EVERY see() report that fires
|
|
29
|
+
# later in the same request. It is fiber-local (concurrent requests never bleed)
|
|
30
|
+
# and the Rack middleware clears it at the end of each request.
|
|
20
31
|
#
|
|
21
32
|
# If you don't know the consequence of an exception, don't catch it.
|
|
22
33
|
|
|
@@ -211,27 +222,65 @@ module Shipeasy
|
|
|
211
222
|
end
|
|
212
223
|
alias causesThe causes_the
|
|
213
224
|
|
|
225
|
+
# Attach debugging metadata. Chainable — call repeatedly (keys merge,
|
|
226
|
+
# later wins) *before* `.to`. Called after `.to` it is a no-op with a
|
|
227
|
+
# warning: the report already shipped, so there is nothing to amend and,
|
|
228
|
+
# crucially, it must not raise into the caller's rescue block. Use
|
|
229
|
+
# `.to(outcome, extras)` or `Shipeasy.add_extras` for late/scattered
|
|
230
|
+
# context instead.
|
|
214
231
|
def extras(extras)
|
|
215
|
-
if
|
|
216
|
-
|
|
232
|
+
if @done
|
|
233
|
+
Shipeasy::Logging.warn(
|
|
234
|
+
"[shipeasy] see() .extras(...) called after .to(...) is ignored — " \
|
|
235
|
+
"pass extras to .to(outcome, extras) or call .extras before .to"
|
|
236
|
+
)
|
|
237
|
+
return self
|
|
217
238
|
end
|
|
239
|
+
merge_extras(extras)
|
|
218
240
|
self
|
|
219
241
|
end
|
|
220
242
|
|
|
221
243
|
# Terminal: build the event and fire-and-forget the report. Idempotent.
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
# `extras` may be passed inline here as the trailing form
|
|
245
|
+
# `.to(outcome, { order_id: oid })` — merged like a final `.extras` call.
|
|
246
|
+
# Returns self so a stray trailing `.extras` chains harmlessly.
|
|
247
|
+
def to(outcome, extras = nil)
|
|
248
|
+
return self if @done
|
|
224
249
|
|
|
250
|
+
merge_extras(extras) unless extras.nil?
|
|
225
251
|
@done = true
|
|
226
252
|
@outcome = outcome.to_s
|
|
227
253
|
begin
|
|
228
254
|
@dispatch.call(
|
|
229
|
-
Built.new(@problem, @subject || DEFAULT_SUBJECT, @outcome.empty? ? DEFAULT_OUTCOME : @outcome,
|
|
255
|
+
Built.new(@problem, @subject || DEFAULT_SUBJECT, @outcome.empty? ? DEFAULT_OUTCOME : @outcome, resolved_extras)
|
|
230
256
|
)
|
|
231
257
|
rescue StandardError
|
|
232
258
|
# Reporting must never raise into caller code.
|
|
233
259
|
nil
|
|
234
260
|
end
|
|
261
|
+
self
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
private
|
|
265
|
+
|
|
266
|
+
def merge_extras(extras)
|
|
267
|
+
return unless extras.is_a?(Hash) && !extras.empty?
|
|
268
|
+
|
|
269
|
+
@extras = (@extras || {}).merge(extras)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The chain's own extras merged OVER the ambient per-request buffer, so a
|
|
273
|
+
# chained key of the same name wins over an ambient one. Keys normalized
|
|
274
|
+
# to strings only when a merge actually happens; sanitize_extras
|
|
275
|
+
# stringifies the rest at build time.
|
|
276
|
+
def resolved_extras
|
|
277
|
+
ambient = Context.current
|
|
278
|
+
return @extras if ambient.empty?
|
|
279
|
+
return ambient if @extras.nil? || @extras.empty?
|
|
280
|
+
|
|
281
|
+
out = ambient.dup
|
|
282
|
+
@extras.each { |k, v| out[k.to_s] = v }
|
|
283
|
+
out
|
|
235
284
|
end
|
|
236
285
|
end
|
|
237
286
|
|
|
@@ -265,6 +314,8 @@ module Shipeasy
|
|
|
265
314
|
end
|
|
266
315
|
|
|
267
316
|
# A no-op chain returned by the module-level see() when no client exists.
|
|
317
|
+
# Every method returns self so the full grammar — including a trailing
|
|
318
|
+
# `.extras` after `.to` — chains without ever raising.
|
|
268
319
|
class NullChain
|
|
269
320
|
def causes_the(_subject)
|
|
270
321
|
self
|
|
@@ -275,9 +326,54 @@ module Shipeasy
|
|
|
275
326
|
self
|
|
276
327
|
end
|
|
277
328
|
|
|
278
|
-
def to(_outcome)
|
|
329
|
+
def to(_outcome, _extras = nil)
|
|
330
|
+
self
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# ---- Ambient per-request extras -------------------------------------
|
|
335
|
+
|
|
336
|
+
# A per-request buffer of extras that merge into EVERY see() report firing
|
|
337
|
+
# later in the same execution context. Lets a request attach context
|
|
338
|
+
# (order id, route, tenant) from anywhere without threading it into the
|
|
339
|
+
# rescue block: `Shipeasy.add_extras(order_id: oid)` here, and any
|
|
340
|
+
# subsequent `see()` in this request carries it.
|
|
341
|
+
#
|
|
342
|
+
# Fiber-local (like AnonId), so concurrent requests never bleed into each
|
|
343
|
+
# other. The Rack middleware clears it in an `ensure` at the end of each
|
|
344
|
+
# request; outside a Rack request (jobs, scripts) call
|
|
345
|
+
# `Shipeasy.clear_extras` yourself when a logical unit of work ends.
|
|
346
|
+
#
|
|
347
|
+
# Values are stored raw and sanitized (scalar-only, truncated, 20-key cap,
|
|
348
|
+
# private-attribute stripped) at build time, exactly like chained extras.
|
|
349
|
+
module Context
|
|
350
|
+
THREAD_KEY = :shipeasy_see_ambient_extras
|
|
351
|
+
|
|
352
|
+
module_function
|
|
353
|
+
|
|
354
|
+
# Merge fields into the current context's buffer (string keys, later
|
|
355
|
+
# wins). Non-hash / empty input is ignored. Never raises.
|
|
356
|
+
def add(extras)
|
|
357
|
+
return unless extras.is_a?(Hash) && !extras.empty?
|
|
358
|
+
|
|
359
|
+
buf = (Thread.current[THREAD_KEY] ||= {})
|
|
360
|
+
extras.each { |k, v| buf[k.to_s] = v }
|
|
361
|
+
nil
|
|
362
|
+
rescue StandardError
|
|
279
363
|
nil
|
|
280
364
|
end
|
|
365
|
+
|
|
366
|
+
# A copy of the current context's buffer, or {} when empty.
|
|
367
|
+
def current
|
|
368
|
+
buf = Thread.current[THREAD_KEY]
|
|
369
|
+
buf.nil? || buf.empty? ? {} : buf.dup
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Drop the current context's buffer so extras never leak to the next
|
|
373
|
+
# request handled by this thread/fiber.
|
|
374
|
+
def clear
|
|
375
|
+
Thread.current[THREAD_KEY] = nil
|
|
376
|
+
end
|
|
281
377
|
end
|
|
282
378
|
end
|
|
283
379
|
end
|
data/lib/shipeasy/sdk/version.rb
CHANGED
data/lib/shipeasy-sdk.rb
CHANGED
|
@@ -76,5 +76,34 @@ module Shipeasy
|
|
|
76
76
|
def self.control_flow_exception(err)
|
|
77
77
|
See::ControlFlowChain.new(err)
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
# ---- ambient per-request see() extras -------------------------------
|
|
81
|
+
#
|
|
82
|
+
# Attach context that merges into every see() report firing later in this
|
|
83
|
+
# request, from anywhere — no need to thread it into the rescue block:
|
|
84
|
+
#
|
|
85
|
+
# Shipeasy::SDK.add_extras(order_id: order.id, tenant: tenant.slug)
|
|
86
|
+
# # ...later, somewhere else in the same request...
|
|
87
|
+
# rescue => e
|
|
88
|
+
# Shipeasy::SDK.see(e).causes_the("checkout").to("use cached prices")
|
|
89
|
+
# # ^ report carries order_id + tenant automatically
|
|
90
|
+
#
|
|
91
|
+
# Fiber-local, so concurrent requests never bleed. The Rack middleware
|
|
92
|
+
# clears the buffer per request (Rails auto-mounts it); outside Rack, call
|
|
93
|
+
# `clear_extras` when a unit of work ends. Accepts a hash and/or keywords.
|
|
94
|
+
# Works with no client configured (it only writes the buffer); never raises.
|
|
95
|
+
def self.add_extras(extras = nil, **kwargs)
|
|
96
|
+
merged = {}
|
|
97
|
+
merged.merge!(extras) if extras.is_a?(Hash)
|
|
98
|
+
merged.merge!(kwargs) unless kwargs.empty?
|
|
99
|
+
See::Context.add(merged)
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Drop the ambient extras buffer for the current request/context.
|
|
104
|
+
def self.clear_extras
|
|
105
|
+
See::Context.clear
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
79
108
|
end
|
|
80
109
|
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: 3.
|
|
4
|
+
version: 3.4.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-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|