@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.
|
|
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.
|
|
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.
|
|
201
|
+
result = Spree.return_create_workflow.call(order: order, items: items)
|
|
183
202
|
result.success? # => false
|
|
184
|
-
result.error.
|
|
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.
|
|
259
|
-
the same hooks as their cart
|
|
260
|
-
|
|
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
|
|
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.
|