@spree/docs 0.1.157 → 0.1.159

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.
@@ -0,0 +1,173 @@
1
+ ---
2
+ title: Validations
3
+ section: customization
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ Adding a rule — and occasionally relaxing one Spree ships — is one of the most
9
+ common reasons to reach into core models. There are three ways to do it, and
10
+ picking the right one is mostly a question of *what kind of rule* you have.
11
+
12
+ | Your rule is about… | Use |
13
+ |---|---|
14
+ | Whether an **operation** may happen — a purchase limit, B2B eligibility, a region policy | A workflow [`validate` hook](workflows.md) |
15
+ | The **shape of a record** — a field you added must be present, a format must match | A [decorator](decorators.md) adding a validation |
16
+ | A **new attribute** you need on an existing model | [Custom fields](../core-concepts/metafields.md) |
17
+
18
+ Spree deliberately has no registry for adding or removing arbitrary model
19
+ validations. Adding one with a decorator is already a single line of ordinary
20
+ Rails, and removing a core rule is served by the knobs below.
21
+
22
+ ## Vetoing an operation
23
+
24
+ This is the one to reach for first. A `validate` hook runs inside the flow,
25
+ before anything is written, and can stop it:
26
+
27
+ ```ruby
28
+ # config/initializers/spree.rb
29
+ Spree.hooks.register('carts.add_item.validate', 'MyStore::CheckPurchaseLimit')
30
+ Spree.hooks.register('carts.upsert_items.validate', 'MyStore::CheckPurchaseLimit')
31
+
32
+ module MyStore
33
+ class CheckPurchaseLimit
34
+ def call(workflow)
35
+ return if workflow.quantity <= 10
36
+
37
+ workflow.errors.add(:quantity, :purchase_limit_exceeded,
38
+ message: 'You can order at most 10 of this item.')
39
+ workflow.reject!
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ Why this beats a model validation for operational rules: it runs only for the
46
+ operation you targeted, so a data migration or an admin correction isn't fought
47
+ by a customer-facing rule; it sees the whole flow, not one record; and it can
48
+ tell staff from customers (`workflow.created_by`) to allow a supervisor
49
+ override.
50
+
51
+ Both cart keys appear above because adding to a cart and setting a quantity are
52
+ different flows. See [Services & Workflows](workflows.md)
53
+ for the full hook list.
54
+
55
+ ## Adding a rule to a model
56
+
57
+ For rules about the shape of a record, a decorator is the supported route:
58
+
59
+ ```ruby
60
+ # app/models/spree/product_decorator.rb
61
+ module MyStore
62
+ module ProductDecorator
63
+ def self.prepended(base)
64
+ base.validates :external_id, presence: true, uniqueness: true
65
+ end
66
+ end
67
+ end
68
+
69
+ Spree::Product.prepend(MyStore::ProductDecorator)
70
+ ```
71
+
72
+ Decorators couple you to Spree internals, so prefer a hook when the rule is
73
+ really about an operation. See
74
+ [Decorators](decorators.md).
75
+
76
+ ## Relaxing a rule Spree ships
77
+
78
+ Never call `clear_validators!` — it wipes every validation on the model,
79
+ including ones added by other extensions, and the breakage appears far from the
80
+ cause. Use one of these instead.
81
+
82
+ ### Store preferences
83
+
84
+ Some rules are already switchable, per store, from the dashboard or a console:
85
+
86
+ | Preference | Effect |
87
+ |---|---|
88
+ | `address_requires_phone` | Requires a phone number on addresses |
89
+ | `company_field_enabled` | Shows the company field on address forms |
90
+ | `address_requires_company` | Requires it — only meaningful with the field shown |
91
+ | `disable_sku_validation` | Turns off SKU uniqueness |
92
+
93
+ ```ruby
94
+ store.update!(preferred_address_requires_company: true)
95
+ ```
96
+
97
+ ### Overriding the gating predicate
98
+
99
+ Most conditional validations in `Spree::Address` are gated on a predicate you
100
+ can override, which is far safer than removing the validation:
101
+
102
+ ```ruby
103
+ module MyStore
104
+ module AddressDecorator
105
+ # Business addresses only in this store.
106
+ def require_company?
107
+ !quick_checkout
108
+ end
109
+ end
110
+ end
111
+
112
+ Spree::Address.prepend(MyStore::AddressDecorator)
113
+ ```
114
+
115
+ `require_name?`, `require_street?`, `require_zipcode?`, `require_phone?` and
116
+ `require_company?` all work this way.
117
+
118
+ ### The address validator registry
119
+
120
+ Address rules are regional and business-specific, so addresses carry a registry
121
+ of extra validator classes you can add to and remove from:
122
+
123
+ ```ruby
124
+ # config/initializers/spree.rb — or config.to_prepare for your own classes
125
+ Spree.validators.addresses.register(MyStore::PostBoxValidator)
126
+
127
+ # Drop a rule Spree ships
128
+ Spree.validators.addresses.unregister(Spree::Addresses::PhoneValidator)
129
+ ```
130
+
131
+ A validator is an ordinary `ActiveModel::Validator`:
132
+
133
+ ```ruby
134
+ module MyStore
135
+ class PostBoxValidator < ActiveModel::Validator
136
+ def validate(record)
137
+ return unless record.address1.to_s.match?(/\A\s*P\.?O\.? Box/i)
138
+
139
+ record.errors.add(:address1, :po_box_not_deliverable)
140
+ end
141
+ end
142
+ end
143
+ ```
144
+
145
+ Register your own classes from `config.to_prepare` rather than an initializer —
146
+ the registry holds classes, and a reloadable constant registered once at boot
147
+ goes stale on the next reload.
148
+
149
+ ## Checkout requirements
150
+
151
+ Rules about what a cart needs *before it can be completed* — a phone number
152
+ before delivery, a purchase order number for B2B — belong in the checkout
153
+ requirements registry rather than a model validation, because they also drive
154
+ what the storefront shows as outstanding:
155
+
156
+ ```ruby
157
+ Spree::Checkout::Registry.add_requirement(
158
+ step: 'address',
159
+ field: 'phone',
160
+ message: 'Phone number is required for delivery',
161
+ satisfied: ->(cart) { cart.ship_address&.phone.present? }
162
+ )
163
+ ```
164
+
165
+ See the [Spree 6 quickstart](/v6/developer/customization/quickstart) for the full registry API.
166
+
167
+ ## Custom field values
168
+
169
+ Custom fields currently validate their type only — a `Number` field rejects
170
+ non-numbers, a `Json` field rejects malformed JSON. There is no per-definition
171
+ length, range or format rule yet. A product type marking a custom field as
172
+ *required* is an advisory marker shown in the dashboard; the server does not
173
+ enforce it.
@@ -37,7 +37,7 @@ result = Spree.cart_add_item_workflow.call(cart: cart, variant: variant, quantit
37
37
  if result.success?
38
38
  line_item = result.value
39
39
  else
40
- puts result.error.value
40
+ puts result.error.to_s
41
41
  end
42
42
  ```
43
43
 
@@ -76,12 +76,31 @@ module MyStore
76
76
  # every #perform keyword is a reader: cart, variant, quantity, ...
77
77
  return if workflow.quantity <= 10
78
78
 
79
- workflow.reject!('You can order at most 10 of this item.')
79
+ workflow.errors.add(:quantity, :purchase_limit_exceeded,
80
+ message: 'You can order at most 10 of this item.')
81
+ workflow.reject!
80
82
  end
81
83
  end
82
84
  end
83
85
  ```
84
86
 
87
+ `workflow.errors` is an `ActiveModel::Errors`, the same object a model uses. A
88
+ rejection therefore reaches the API in the shape clients already handle for
89
+ validation failures — the field name, a symbolic code, and the message:
90
+
91
+ ```json
92
+ {
93
+ "error": {
94
+ "code": "validation_error",
95
+ "message": "Quantity You can order at most 10 of this item.",
96
+ "details": { "quantity": ["You can order at most 10 of this item."] }
97
+ }
98
+ }
99
+ ```
100
+
101
+ Add to `:base` for a rejection that isn't about one field. `reject!('message')`
102
+ with an argument is shorthand for exactly that.
103
+
85
104
  > **NOTE:** Hook keys are validated at boot. Registering against a hook that doesn't exist
86
105
  > raises `Spree::Hooks::UnknownHookError` with the list of valid hooks for that
87
106
  > workflow, so a typo fails immediately instead of silently never firing.
@@ -179,9 +198,10 @@ The caller receives a normal failure result — no exception reaches your
179
198
  controller:
180
199
 
181
200
  ```ruby
182
- result = Spree.cart_add_item_workflow.call(cart: cart, variant: variant)
201
+ result = Spree.return_create_workflow.call(order: order, items: items)
183
202
  result.success? # => false
184
- result.error.value # => "You can order at most 10 of this item."
203
+ result.error.to_s # => "This order is outside the 30-day return window."
204
+ result.error.value # => ActiveModel::Errors — the rejection, field by field
185
205
  ```
186
206
 
187
207
  > **WARNING:** Reject from `validate` hooks, not from `after_*` hooks. Rejecting late still
@@ -227,6 +247,8 @@ is reported through `Rails.error` so it's visible rather than mysterious.
227
247
  |---|---|---|---|
228
248
  | `carts.add_item` | `validate` | validate | Before the line item is built |
229
249
  | `carts.add_item` | `after_item_added` | lifecycle | After the item is saved and totals recalculated (in transaction) |
250
+ | `carts.upsert_items` | `validate` | validate | Once per item, immediately before that item is applied — quantity edits, removals and bulk payloads. Earlier items in the batch may already be written |
251
+ | `carts.upsert_items` | `after_items_upserted` | lifecycle | After the batch is applied and the cart recalculated once (in transaction) |
230
252
  | `carts.complete` | `validate` | validate | After checkout requirements pass, before the order is created |
231
253
  | `carts.complete` | `before_finalize` | lifecycle | After payment, before the order is placed |
232
254
  | `carts.complete` | `after_finalize` | lifecycle | After the order is placed |
@@ -251,13 +273,33 @@ is reported through `Rails.error` so it's visible rather than mysterious.
251
273
  | `payments.handle_webhook` | `after_handle` | lifecycle | After the gateway callback is processed |
252
274
  | `customers.create` | `validate` | validate | After the customer is built, before it is saved — the registration-policy veto (bot screening, B2B approval) |
253
275
  | `customers.create` | `after_create` | lifecycle | After the customer is created and the newsletter subscriber linked |
276
+ | `products.create` | `validate` | validate | After the product is built, before it is saved — fires for the Admin API, CSV imports and seeds alike |
277
+ | `products.create` | `after_create` | lifecycle | After the product is saved (in transaction) |
278
+ | `products.update` | `validate` | validate | With the pending attributes assigned, so `product.changes` describes the edit |
279
+ | `products.update` | `after_update` | lifecycle | After the product is saved (in transaction) |
280
+ | `products.destroy` | `validate` | validate | Before the product is soft-deleted — refuse a deletion your store shouldn't allow |
281
+ | `products.destroy` | `after_destroy` | lifecycle | After the soft-delete, for host cleanup (in transaction) |
254
282
 
255
283
  `before_cancel` and `before_resume` accept `reject!` like a `validate` hook.
256
284
 
257
285
  Draft-order editing in the admin uses **twin workflows** with their own keys —
258
- `orders.add_item`, `orders.recalculate`, `orders.recalculate_totals` — carrying
259
- the same hooks as their cart counterparts. Register against the cart key for
260
- storefront carts, the order key for admin edits, or both.
286
+ `orders.add_item`, `orders.upsert_items`, `orders.recalculate`,
287
+ `orders.recalculate_totals` — carrying the same hooks as their cart
288
+ counterparts. Register against the cart key for storefront carts, the order key
289
+ for admin edits, or both.
290
+
291
+ > **NOTE:** `carts.add_item` **adds** to a quantity; `carts.upsert_items` **sets** it, and
292
+ > is what a quantity edit, a removal (quantity `0`) and a bulk item payload all
293
+ > run through. A rule about what may be in a cart therefore belongs on both keys —
294
+ > the readers are the same (`cart`, `variant`, `quantity`), so one handler class
295
+ > registers against each.
296
+ >
297
+ > `upsert_items` is also the one flow where a rejection is **not** fatal on the
298
+ > storefront: the vetoed item is skipped, the rest of the batch applies, and what
299
+ > was dropped comes back in the cart's `warnings`. A customer restoring a saved
300
+ > cart keeps whatever is still purchasable. Admin order edits behave the opposite
301
+ > way — the whole edit fails — because a silently dropped row is worse than a
302
+ > failed request when a merchant is editing.
261
303
 
262
304
  Inspect what's available at runtime:
263
305
 
@@ -340,7 +382,7 @@ The whole vocabulary:
340
382
  | `on_flow_failure: :name` | Names the undo for a step, run in reverse if a later step fails |
341
383
  | `run_hooks :name` | Dispatches a declared hook; returns the merged hash from context handlers |
342
384
  | `failure(value, error)` | Aborts the flow — rolls back an open transaction and returns a failure result |
343
- | `reject!(message)` | The same, named for hook handlers vetoing a flow |
385
+ | `reject!` | The same, named for hook handlers vetoing a flow — carries `workflow.errors` |
344
386
  | `halt!(value)` | Successful early exit (not valid inside a transaction the workflow opened) |
345
387
  | `hooks :a, :b` | Declares the extension points this workflow dispatches |
346
388
 
@@ -0,0 +1,218 @@
1
+ ---
2
+ title: Telemetry
3
+ description: Distributed tracing with OpenTelemetry — one gem, standard OTEL_* environment variables, and every checkout becomes a trace from HTTP request to gateway call to webhook delivery.
4
+ ---
5
+
6
+ Spree supports [OpenTelemetry](https://opentelemetry.io), the open standard for
7
+ distributed tracing. Install the optional `spree_opentelemetry` gem, point it
8
+ at your collector with the same environment variables every other
9
+ OpenTelemetry service uses, and Spree exports traces — no code changes, no
10
+ vendor lock-in. Traces flow to any OpenTelemetry-compatible backend: Grafana
11
+ Tempo, Jaeger, Datadog, Honeycomb, New Relic, Dynatrace, and others.
12
+
13
+ ## Setup
14
+
15
+ Add the gem to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'spree_opentelemetry'
19
+ ```
20
+
21
+ Then configure the exporter through the standard OpenTelemetry environment
22
+ variables:
23
+
24
+ ```bash
25
+ OTEL_SERVICE_NAME=spree
26
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
27
+ ```
28
+
29
+ That is the entire setup. Without an exporter configured, the gem stays
30
+ dormant and adds no overhead. `OTEL_SDK_DISABLED=true` turns telemetry off
31
+ regardless of any other setting.
32
+
33
+ Other standard variables work as documented in the
34
+ [OpenTelemetry SDK configuration reference](https://opentelemetry.io/docs/languages/sdk-configuration/),
35
+ including sampling:
36
+
37
+ ```bash
38
+ # Sample 10% of traces (children follow their parent's decision)
39
+ OTEL_TRACES_SAMPLER=parentbased_traceidratio
40
+ OTEL_TRACES_SAMPLER_ARG=0.1
41
+ ```
42
+
43
+ ## What gets traced
44
+
45
+ Two layers combine into one trace per request or job.
46
+
47
+ **Framework spans** come from the official Rails auto-instrumentation: HTTP
48
+ requests, controller actions, database queries, background job enqueues and
49
+ executions, mail deliveries, and outbound HTTP calls. Trace context carries
50
+ across the job boundary, so work that happens in a background job stays
51
+ connected to the request that caused it.
52
+
53
+ **Commerce spans** come from Spree itself:
54
+
55
+ | Span | Kind | What it covers |
56
+ |---|---|---|
57
+ | `carts.complete` (any workflow key) | internal | One span per workflow run, with its outcome |
58
+ | `carts.complete process_payments` (any step) | internal / client | One span per workflow step; steps declared as external I/O become client spans |
59
+ | `carts.add_item hooks validate` | internal | Extension hook dispatch, only when handlers are registered |
60
+ | `order.placed dispatch` | internal | Event delivery to each subscriber, showing whether it ran inline or was enqueued |
61
+ | `spree.webhook.deliver order.placed` | client | Each webhook POST, with the destination host and response code |
62
+ | `spree.gateway.purchase` (any gateway action) | client | Each payment gateway call — authorize, purchase, capture, void, credit, and payment session operations |
63
+
64
+ A completed checkout, for example, produces one trace containing the HTTP
65
+ request, the `carts.complete` workflow and its steps, the payment gateway
66
+ call, the database work, and — linked from it — the background jobs and
67
+ webhook deliveries the order triggered.
68
+
69
+ Spree also propagates
70
+ [W3C Trace Context](https://www.w3.org/TR/trace-context/) headers on outbound
71
+ webhooks, so a system receiving your webhooks can join its own spans to the
72
+ trace that produced the event.
73
+
74
+ ## Span attributes and personal data
75
+
76
+ Span attributes never contain personal or sensitive data. They are limited to
77
+ workflow and step names, gateway action names, payment method class names,
78
+ event names, webhook destination hosts, and HTTP status codes. Order contents,
79
+ customer emails, addresses, payment details, and webhook payloads are never
80
+ attached to spans.
81
+
82
+ ## Metrics
83
+
84
+ Spree exports the trace signal. Request rates, error rates, and latency
85
+ percentiles per endpoint, workflow, or gateway are derived from spans in the
86
+ OpenTelemetry Collector with the
87
+ [span metrics connector](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/spanmetricsconnector):
88
+
89
+ ```yaml
90
+ # otel-collector config
91
+ connectors:
92
+ spanmetrics:
93
+ dimensions:
94
+ - name: spree.workflow
95
+ - name: spree.gateway.action
96
+
97
+ service:
98
+ pipelines:
99
+ traces:
100
+ receivers: [otlp]
101
+ exporters: [spanmetrics, otlp]
102
+ metrics:
103
+ receivers: [spanmetrics]
104
+ exporters: [prometheusremotewrite]
105
+ ```
106
+
107
+ ## Using with Sentry
108
+
109
+ Sentry and OpenTelemetry are complementary — Sentry's error capture works
110
+ independently of tracing, so having both installed (as spree-starter does)
111
+ requires no special setup. For traces there are three arrangements:
112
+
113
+ **Sentry for errors, OpenTelemetry for traces (default).** Nothing to
114
+ configure. Just don't *also* enable Sentry's own performance tracing
115
+ (`traces_sample_rate`) — that would instrument every request twice and
116
+ produce two disconnected trace systems.
117
+
118
+ **Sentry as the trace backend.** Sentry ingests OpenTelemetry spans directly
119
+ through its [OTLP integration](https://docs.sentry.io/platforms/ruby/guides/rails/integrations/otlp/).
120
+ Order matters here: Sentry registers its span processor inside `Sentry.init`,
121
+ which only works if the OpenTelemetry SDK is already installed — so install
122
+ Spree's telemetry explicitly at the top of the same initializer:
123
+
124
+ ```ruby
125
+ # Gemfile
126
+ gem 'sentry-opentelemetry'
127
+
128
+ # config/initializers/sentry.rb
129
+ SpreeOpenTelemetry.configure { |config| config.enabled = true }
130
+ SpreeOpenTelemetry.install!
131
+
132
+ Sentry.init do |config|
133
+ config.dsn = ENV['SENTRY_DSN']
134
+ config.otlp.enabled = true
135
+ # Do not set traces_sample_rate or instrumenter — OpenTelemetry owns tracing.
136
+ end
137
+ ```
138
+
139
+ ```bash
140
+ # Sentry provides the exporter (derived from the DSN) — tell the SDK not to
141
+ # wire its own default OTLP exporter alongside it.
142
+ OTEL_TRACES_EXPORTER=none
143
+ ```
144
+
145
+ A DSN alone does **not** enable tracing; `config.otlp.enabled` is the
146
+ explicit opt-in (Sentry bills for ingested spans, so error capture never
147
+ silently becomes span ingestion).
148
+
149
+ Spree's commerce spans — workflows, gateway calls, webhook deliveries — show
150
+ up in Sentry's trace view, and Sentry errors are linked automatically to the
151
+ span that was active when they were captured.
152
+
153
+ **Both, via the collector.** Point Spree at an OpenTelemetry Collector and
154
+ fan out from there — one pipeline exporting to your tracing backend and
155
+ another to Sentry's OTLP endpoint. This is the most flexible arrangement for
156
+ teams that want Grafana/Jaeger for latency work and Sentry for error triage
157
+ over the same traces.
158
+
159
+ ## Correlating logs
160
+
161
+ To connect log lines to traces, tag your Rails logs with the current trace:
162
+
163
+ ```ruby
164
+ # config/environments/production.rb
165
+ config.log_tags = [
166
+ ->(_request) { "trace_id=#{OpenTelemetry::Trace.current_span.context.hex_trace_id}" }
167
+ ]
168
+ ```
169
+
170
+ ## Trying it locally
171
+
172
+ Run Jaeger with an OTLP receiver and point Spree at it:
173
+
174
+ ```bash
175
+ docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest
176
+ ```
177
+
178
+ ```bash
179
+ OTEL_SERVICE_NAME=spree \
180
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
181
+ bin/rails server
182
+ ```
183
+
184
+ Place a test order and open [http://localhost:16686](http://localhost:16686)
185
+ to see the trace.
186
+
187
+ ## Code-level configuration
188
+
189
+ Everything routine is controlled by environment variables. A
190
+ `SpreeOpenTelemetry.configure` block exists for the rest — adding
191
+ instrumentation for libraries your app uses, removing a default, or advanced
192
+ SDK tuning:
193
+
194
+ ```ruby
195
+ # config/initializers/opentelemetry.rb
196
+ SpreeOpenTelemetry.configure do |config|
197
+ config.use 'OpenTelemetry::Instrumentation::Redis' # add an instrumentation
198
+ config.skip 'OpenTelemetry::Instrumentation::ActionMailer' # remove a default
199
+ config.with_sdk { |otel| otel.add_span_processor(my_processor) }
200
+ end
201
+ ```
202
+
203
+ ## Instrumenting your own code
204
+
205
+ Spree's spans are built on `ActiveSupport::Notifications`, and yours can be
206
+ too — or use the OpenTelemetry API directly:
207
+
208
+ ```ruby
209
+ tracer = OpenTelemetry.tracer_provider.tracer('my_app')
210
+
211
+ tracer.in_span('loyalty.award_points', attributes: { 'loyalty.points' => 50 }) do
212
+ # your code
213
+ end
214
+ ```
215
+
216
+ Custom workflows get traced automatically: every `Spree::Workflow` run, step,
217
+ and hook dispatch is instrumented by the framework, including workflows your
218
+ application or extensions define.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.157",
3
+ "version": "0.1.159",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",