Skip to content

feat(observability): URL routers + per-call operation names + commerce histograms#48

Merged
vibe-dex merged 7 commits into
nextfrom
feat/observability-operations
May 18, 2026
Merged

feat(observability): URL routers + per-call operation names + commerce histograms#48
vibe-dex merged 7 commits into
nextfrom
feat/observability-operations

Conversation

@vibe-dex

@vibe-dex vibe-dex commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds end-to-end semantic operation naming on outbound VTEX + Shopify fetches and emits a provider-labelled commerce duration histogram. Builds on the operation API shipped in @decocms/start@5.3.0-rc.0 (deco-start#179).

After this lands, every outbound commerce call from a storefront produces:

  • A span named vtex.<operation> / shopify.<operation> (e.g. vtex.checkout.orderform.items.add, shopify.ProductDetails) instead of a generic vtex.fetch.
  • A fetch.operation attribute on the span so the same string is queryable independently of span name.
  • A commerce_request_duration_ms histogram record with labels { provider, operation, status_code, cached }.
  • W3C traceparent injection (already in the framework).
  • URL redaction on log + span attributes (already in the framework).

Zero behavioral change for sites that don't opt in — setVtexFetch(createVtexFetch()) is the one-line opt-in.

What's in here

  1. chore(deps): bump @decocms/start devDep → 5.3.0-rc.0, peerDep → >=5.3.0-rc.0.

  2. feat(vtex): vtex/utils/operationRouter.ts — pure (url, method) => operation | undefined. Covers IS, checkout/orderForm (incl. POST/PATCH/DELETE differentiation on /items), sessions, segments, catalog, masterdata (entity-suffixed), OMS, vtexid, IO GraphQL, sitemap. 22 vitest cases.

  3. feat(shopify): shopify/utils/operationRouter.ts + graphqlOperationName.ts — URL router + GraphQL document parser. Names spans shopify.<OperationName> (extracted from query Foo { ... }) instead of the generic shopify.storefront.graphql. Wired into createGraphqlClient. 11 vitest cases.

  4. feat(factories): createVtexFetch() / createShopifyFetch() — pre-wired InstrumentedFetch factories bundling createInstrumentedFetch + URL router + onComplete: commerce_request_duration_ms. 10 vitest cases.

  5. refactor(vtex): funnel 9 server-side ad-hoc fetches through the configured _fetch:

    • performVtexLogoutvtexid.logout
    • proxyToVtex / createVtexCheckoutProxy → URL-router
    • notifyMe / subscribe (.aspx) → notifyme / newsletter.subscribe
    • sendEvent × 2 (sp.vtex.com) → analytics.event
    • updateProfileFromRequestio.graphql.UpdateProfile
    • uploadAttachmentmasterdata.attachment.upload

    Adds getVtexFetch() accessor + widens setVtexFetch to accept InstrumentedFetch | typeof fetch + widens vtexFetch* helpers to take InstrumentedFetchInit so the operation field threads through.

  6. docs(readme): setup recipe + subpath table.

Out of scope (follow-ups)

  • Resend (resend/actions/send.ts) and Google Fonts (website/loaders/fonts/googleFonts.ts) raw fetches — non-VTEX/non-Shopify hosts, low traffic; left as raw fetch to keep PR scope focused. Easy follow-up if/when needed.
  • Client-side React hooks (vtex/hooks/use*.ts) intentionally untouched. They run in the browser against same-origin /api/..., which is proxied server-side by proxyToVtex / createVtexCheckoutProxy (now instrumented). Wiring an instrumented fetch in the browser gives no tracer / no metrics — wasted layer.
  • Pre-existing test failure on main: vtex/__tests__/client-segment-cookie.test.ts has 2 cases that fail under Node 22 / undici (which treats cookie as a forbidden request header on new Request()). Not introduced by this PR — fails identically with the dep bump stashed. Worth a separate fix/segment-cookie-test to rewrite the fixture before promoting next → main.

Test plan

  • npm run typecheck — clean
  • npm run lint — clean
  • npm test — 411 / 413 passing; the 2 failures are the pre-existing cookie tests called out above
  • npx knip — no new findings (and removed one stale ignoreDependencies entry)
  • Validate prerelease on a real storefront once semantic-release publishes 1.x.0-next.X from this branch
  • Confirm spans land in HyperDX with operation names + the commerce_request_duration_ms histogram shows up in ClickHouse / Grafana

Coordinated release path

This is the apps-start half of a three-PR observability rollout:

  1. stats-lake #1POST /v1/metrics + OTLP metrics DDLs. Merged + deployed.
  2. deco-start #179 — operation API + direct-POST channels. Published manually as 5.3.0-rc.0 on the @rc dist-tag (deco-start's next branch is currently blocked by unrelated unfinished work). Will be promoted to 5.3.0 on main once this rc soaks.
  3. This PR — once merged to next, semantic-release publishes @decocms/apps@1.x.0-next.X on the @next tag. Then bump a real storefront to both prereleases and validate end-to-end.

Made with Cursor


Summary by cubic

Adds semantic operation names and a commerce_request_duration_ms histogram for all outbound VTEX and Shopify calls. Provides pre-wired fetch factories and URL routers; Shopify GraphQL calls are named per operation.

  • New Features

    • createVtexFetch() and createShopifyFetch() pre-wire spans, traceparent, URL redaction, and a histogram with { provider, operation, status_code, cached }.
    • URL routers: vtexOperationRouter and shopifyOperationRouter map URLs to low-cardinality operations.
    • Shopify: extractGraphqlOperationName stamps init.operation, so spans become shopify.<OperationName>; wired into createGraphqlClient.
    • VTEX: new getVtexFetch() accessor; routed ad-hoc server calls through the configured fetch; setVtexFetch now accepts InstrumentedFetch | fetch; VTEX helpers accept InstrumentedFetchInit so operation propagates.
  • Migration

    • Requires @decocms/start@>=5.3.0-rc.0.
    • Opt in:
      • VTEX: setVtexFetch(createVtexFetch())
      • Shopify: setShopifyFetch(createShopifyFetch())

Written for commit 24d0502. Summary will update on new commits. Review in cubic

vibe-dex and others added 7 commits May 14, 2026 14:39
release: promote 1.14.0-next.1 to stable
Unlocks the operation API on `createInstrumentedFetch`
(init.operation / defaultOperation / resolveOperation) shipped in
deco-start PR #179, which the upcoming VTEX + Shopify URL routers and
per-call operation overrides in this PR depend on.

- devDep: ^2.5.0 -> 5.3.0-rc.0 (exact pin to the rc we're validating)
- peerDep: >=0.19.0 -> >=5.3.0-rc.0 (consumers must opt into the rc
  before installing the @decocms/apps next channel that depends on it)

Co-authored-by: Cursor <cursoragent@cursor.com>
Pure `(url, method) => operation | undefined` function pluggable into
`createInstrumentedFetch`'s `resolveOperation` option (shipped in
@decocms/start 5.3.0-rc.0). The resolved string becomes the span suffix
(`vtex.<operation>`), the `fetch.operation` span attribute, and the
histogram label downstream — so it's deliberately low-cardinality and
stable across deploys.

Covers the VTEX API surface this repo actually exercises:

- Intelligent Search (product_search, facets, top_searches, suggestions,
  correction, banners, autocomplete) — endpoint becomes the suffix
- Checkout / orderForm — POST/PATCH/DELETE on `/items` are differentiated
  into add / update / remove; coupons, profile, shippingData, paymentData,
  simulation, regions, postal-code are split off; the singleton
  orderForm/{id} root distinguishes GET vs PATCH
- Sessions (get vs update by method), segments
- Catalog System — pagetype, crossselling.{type}, products.variations,
  products.search, facets.search, category.tree, sku, specification,
  brand, falls back to catalog.other
- Masterdata (entity name encoded into the operation suffix)
- OMS (orders, orders.cancel, orders.pvt)
- VTEX ID (logout, authentication.start/validate, user, vtexid.other)
- IO GraphQL (POST to `_v/private/graphql/v1`), IO segment, sitemap.xml,
  events, license-manager

22 vitest cases cover the matchers + edge cases (unparseable URLs,
case-insensitive methods, query-string stripping, fallback to
`undefined`). Callers that need finer granularity than the URL can
express (same URL, semantically different ops) override per-call via
`init.operation`, which always wins over the router.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds two paired primitives for Shopify outbound observability:

- `shopify/utils/operationRouter.ts` — same shape as the VTEX router;
  pluggable into `createInstrumentedFetch.resolveOperation`. Maps the
  storefront / admin GraphQL endpoints to `storefront.graphql` /
  `admin.graphql` and covers the small REST surface (products, orders,
  customers, inventory, checkouts, cart).

- `shopify/utils/graphqlOperationName.ts` — extracts the semantic
  operation name from a GraphQL document (`query Foo { ... }` →
  `"Foo"`). Strips block strings, string literals, and `# comments`
  before matching so `query` / `mutation` / `subscription` keywords
  inside docstrings/comments don't false-positive. Returns
  `undefined` for anonymous or multi-operation documents (caller must
  disambiguate via explicit name).

Wired into `createGraphqlClient` so every Shopify GraphQL call stamps
`init.operation = <OperationName>` on the outbound fetch. The
framework reads it via the operation API shipped in @decocms/start
5.3.0-rc.0 and produces spans named `shopify.<OperationName>` (e.g.
`shopify.ProductDetails`) instead of the generic
`shopify.storefront.graphql`. The URL router still applies as a
fallback when the extractor can't derive a name.

33 vitest cases cover both helpers including comment/string handling
and a real-world Shopify storefront query shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pre-wired `InstrumentedFetch` factories that bundle the three pieces
sites would otherwise have to assemble by hand:

1. `createInstrumentedFetch` from @decocms/start (spans, traceparent
   injection, URL redaction, cache-header span attributes).
2. The provider's URL router as `resolveOperation`, so unannotated
   callsites still get semantic span operations + histogram labels.
3. An `onComplete` callback that records every call into the
   `commerce_request_duration_ms` histogram via the meter configured
   by `instrumentWorker(...)`. Labels: `provider`, `operation`,
   `status_code`, `cached`.

Sites opt in with one line at startup:

  setVtexFetch(createVtexFetch());
  setShopifyFetch(createShopifyFetch());

`disableHistogram: true` opts out of metric emission while keeping
spans + logs. `baseFetch` lets callers wrap a custom fetch (cookie
passthrough, retry, proxy) and add instrumentation on top.

Both factories + the underlying URL routers are re-exported from
`@decocms/apps/vtex` and `@decocms/apps/shopify` for site
consumption. `extractGraphqlOperationName` is also exposed so sites
can name their own GraphQL clients consistently.

10 vitest cases cover the wiring: histogram emission with correct
labels, URL-router operation propagation, explicit `init.operation`
overrides, `x-cache: HIT` -> `cached=true`, status code propagation,
`disableHistogram` opt-out, and stripping `operation` from the init
before it reaches `baseFetch`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Routes all server-side ad-hoc fetches in vtex/ through the
`setVtexFetch`-controlled `_fetch` instead of raw `globalThis.fetch`,
so sites that opt into the instrumented factory (`createVtexFetch()`)
get spans + commerce_request_duration_ms metrics + traceparent
injection on every outbound VTEX call.

Adds two API surface widenings on `vtex/client.ts`:

- `setVtexFetch` now accepts `typeof fetch | InstrumentedFetch`
  (was `typeof fetch`). A plain fetch still works for uninstrumented
  test/dev setups; an `InstrumentedFetch` unlocks `init.operation`
  propagation.
- New `getVtexFetch(): InstrumentedFetch` accessor for callsites
  that can't shape themselves into the `vtexFetch*` helpers — FormData
  uploads, .aspx legacy endpoints, generic proxies, etc. Callers
  stamp a per-call operation string via `init.operation`.
- `vtexFetchResponse / vtexFetch / vtexCachedFetch / vtexFetchWithCookies`
  init parameter widened to `InstrumentedFetchInit` so `operation`
  flows through them too.

Migrated callsites with explicit operations:

- `vtex/utils/authHelpers.ts` performVtexLogout       -> vtexid.logout
- `vtex/utils/proxy.ts` proxyToVtex                   -> URL-router
- `vtex/utils/proxy.ts` createVtexCheckoutProxy       -> URL-router
- `vtex/actions/misc.ts` notifyMe (AviseMe.aspx)      -> notifyme
- `vtex/actions/misc.ts` sendEvent (sp.vtex.com)      -> analytics.event
- `vtex/actions/newsletter.ts` subscribe              -> newsletter.subscribe
- `vtex/actions/analytics/sendEvent.ts` IS event      -> analytics.event
- `vtex/actions/profile.ts` updateProfileFromRequest  -> io.graphql.UpdateProfile
- `vtex/actions/masterData.ts` uploadAttachment       -> masterdata.attachment.upload

Untouched (intentional):
- Client-side React hooks (`vtex/hooks/use*.ts`) — they run in the
  browser against same-origin `/api/...` which is proxied by
  `proxyToVtex` / `createVtexCheckoutProxy` on the server, so they
  inherit observability from the proxy span. Wiring an instrumented
  fetch in the browser yields no tracer / no metrics — wasted layer.
- `resend/actions/send.ts`, `website/loaders/fonts/googleFonts.ts` —
  non-VTEX hosts, low traffic; left as raw fetch for now to keep PR
  scope focused. Tracked as follow-ups in observability docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Recipe in the Setup section shows the one-liner each (`createVtexFetch()`
  / `createShopifyFetch()`) plus the rc-version requirement on
  `@decocms/start@>=5.3.0-rc.0`.
- Subpath table now lists the new barrel exports
  (`createVtexFetch`, `vtexOperationRouter`, `createShopifyFetch`,
  `shopifyOperationRouter`, `extractGraphqlOperationName`, `getVtexFetch`).
- knip.json: drop `@decocms/start` from `ignoreDependencies` — it's
  now an actively-imported peer dep (was a knip workaround when the
  package was only referenced in JSDoc).

Co-authored-by: Cursor <cursoragent@cursor.com>
@vibe-dex vibe-dex requested a review from a team May 18, 2026 22:26

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 25 files

Re-trigger cubic

@vibe-dex vibe-dex merged commit 2dc0ac2 into next May 18, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.15.0-next.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant