shipeasy-sdk 3.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60cefd35bba21f86ef6b77da83d0c918744721d9c564066b12e8d2b95dba8740
4
- data.tar.gz: 22b03ac85fbd29bba861770120c2a78bbcbc5069e0e7af0cbe8b1c33b8ebee88
3
+ metadata.gz: c1b1f478bea3279f979e85e04476381f719d3db27aa29ffb23447768ceefc560
4
+ data.tar.gz: c0d4f8f1ca49a6a1b34ac3b1198a3788dc115f640eb5845fb388a66175e1384c
5
5
  SHA512:
6
- metadata.gz: 226eccc8d981ce982ed132eb697c39e37cde9f9d62d9f84970cea6119601696383520bc1f03606abd58b81574be31d26962aedf2e000acfe45e1ac7bdb2c5f2c
7
- data.tar.gz: 1e213dba4152299e0e0ad0d4480f02ca55c108d3fc7e5b70c966612799972a8b450fb24e5a74f4524e719d007994211fa903e6c5b76706012c9ba57f786b547e
6
+ metadata.gz: 94790b0a88b1524f35faf706245339db83908565b5adec3a7f8b9f5775d476cb703ce00ac55a697d79a343a1abe0c94145c2fd693538c1aecbdab81385525148
7
+ data.tar.gz: f35b460dd560167674a1a278c1e76600fd59040016ad06b3129ee77c5dd95d68d57d0c7695ca06dee7f7870f15489655ee20ca644b6d1a24d416fdf280c2daf7
data/docs/skill/SKILL.md CHANGED
@@ -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
 
@@ -326,6 +326,17 @@ module Shipeasy
326
326
  Shipeasy::SDK.control_flow_exception(err)
327
327
  end
328
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
+
329
340
  # Replace the registered global engine + attributes transform (used by the
330
341
  # configure_for_* siblings — unlike configure, they replace so a test suite
331
342
  # can reconfigure between cases). Returns the engine.
@@ -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 onto the next request handled by this thread.
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]
@@ -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 that may be called
16
- # in any order *before* `.to`:
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 extras.is_a?(Hash) && !extras.empty?
216
- @extras = (@extras || {}).merge(extras)
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
- def to(outcome)
223
- return if @done
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, @extras)
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
@@ -1,5 +1,5 @@
1
1
  module Shipeasy
2
2
  module SDK
3
- VERSION = "3.3.0"
3
+ VERSION = "3.4.0"
4
4
  end
5
5
  end
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.3.0
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-10 00:00:00.000000000 Z
11
+ date: 2026-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec