@decocms/apps-salesforce 7.31.7 → 7.32.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/apps-salesforce",
3
- "version": "7.31.7",
3
+ "version": "7.32.1",
4
4
  "type": "module",
5
5
  "description": "Deco commerce app: Salesforce Commerce Cloud integration",
6
6
  "repository": {
@@ -22,8 +22,8 @@
22
22
  "lint:unused": "knip"
23
23
  },
24
24
  "dependencies": {
25
- "@decocms/blocks": "7.31.7",
26
- "@decocms/apps-commerce": "7.31.7"
25
+ "@decocms/blocks": "7.32.1",
26
+ "@decocms/apps-commerce": "7.32.1"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@tanstack/react-start": ">=1.0.0",
package/src/index.ts CHANGED
@@ -25,6 +25,10 @@ export type {
25
25
  SalesforceProduct,
26
26
  } from "./types";
27
27
  export { createHttpClient, type HttpClientOptions } from "./utils/httpClient";
28
+ export {
29
+ type CreateSalesforceFetchOptions,
30
+ createSalesforceFetch,
31
+ } from "./utils/instrumentedFetch";
28
32
  export { parseUserCookie } from "./utils/parseUserCookie";
29
33
  export {
30
34
  createProductTransformer,
@@ -16,6 +16,8 @@
16
16
  * loaders that call `.then(res => res.json())` keep working).
17
17
  */
18
18
 
19
+ import { createSalesforceFetch } from "./instrumentedFetch";
20
+
19
21
  export interface HttpClientOptions {
20
22
  base: string;
21
23
  headers?: Record<string, string> | Headers;
@@ -29,9 +31,22 @@ interface IndexedRouteResponse {
29
31
  headers: Headers;
30
32
  }
31
33
 
34
+ // Lazily-created, module-shared instrumented fetch used as the default
35
+ // transport. Every Salesforce loader instantiates its own `createHttpClient`
36
+ // inline, so defaulting the `fetcher` here is the single chokepoint that
37
+ // instruments ALL Salesforce egress (upstream latency/status → ClickHouse,
38
+ // `provider:"salesforce"`) without touching each loader. Sites that pass an
39
+ // explicit `fetcher` opt out; wrap it with `createSalesforceFetch({ baseFetch })`
40
+ // to keep instrumentation.
41
+ let _defaultFetch: typeof fetch | undefined;
42
+ function defaultInstrumentedFetch(): typeof fetch {
43
+ if (!_defaultFetch) _defaultFetch = createSalesforceFetch();
44
+ return _defaultFetch;
45
+ }
46
+
32
47
  export function createHttpClient<_Routes = unknown>(options: HttpClientOptions) {
33
48
  const base = options.base.replace(/\/$/, "");
34
- const fetchImpl = options.fetcher ?? fetch;
49
+ const fetchImpl = options.fetcher ?? defaultInstrumentedFetch();
35
50
  const defaultHeaders: Record<string, string> =
36
51
  options.headers instanceof Headers
37
52
  ? Object.fromEntries(options.headers.entries())
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pre-wired instrumented fetch factory for Salesforce Commerce Cloud.
3
+ *
4
+ * Mirrors `vtex` / `shopify` / `magento`. Bundles `createInstrumentedFetch`
5
+ * (spans, traceparent, URL redaction), the `salesforceOperationRouter`, and an
6
+ * `onComplete` that records `http.client.request.duration` with
7
+ * `provider: "salesforce"`.
8
+ *
9
+ * Salesforce has no module-global `_fetch`; instead `createHttpClient` defaults
10
+ * its `fetcher` to {@link createSalesforceFetch}, so EVERY loader's egress is
11
+ * instrumented automatically without per-loader wiring. Sites can still pass an
12
+ * explicit `fetcher` to `createHttpClient` (e.g. for cookie passthrough) — wrap
13
+ * it with `createSalesforceFetch({ baseFetch })` to keep the instrumentation.
14
+ */
15
+
16
+ import {
17
+ createInstrumentedFetch,
18
+ type InstrumentedFetch,
19
+ } from "@decocms/blocks/sdk/instrumentedFetch";
20
+ import { recordCommerceMetric, statusClassFor } from "@decocms/blocks/sdk/observability";
21
+ import { salesforceOperationRouter } from "./operationRouter";
22
+
23
+ export interface CreateSalesforceFetchOptions {
24
+ /** Underlying fetch to wrap. Defaults to `globalThis.fetch`. */
25
+ baseFetch?: typeof fetch;
26
+ /** Disable the histogram (spans + logs still emit). Default: false. */
27
+ disableHistogram?: boolean;
28
+ }
29
+
30
+ export function createSalesforceFetch(
31
+ options: CreateSalesforceFetchOptions = {},
32
+ ): InstrumentedFetch {
33
+ const { baseFetch, disableHistogram = false } = options;
34
+ return createInstrumentedFetch({
35
+ name: "salesforce",
36
+ baseFetch,
37
+ resolveOperation: salesforceOperationRouter,
38
+ onComplete: disableHistogram ? undefined : (r) =>
39
+ recordCommerceMetric(r.durationMs, { provider: "salesforce", operation: r.operation, status_class: statusClassFor(r.status), cached: r.cached }),
40
+ });
41
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * URL-derived operation name router for Salesforce Commerce Cloud calls.
3
+ *
4
+ * Plugged into `@decocms/blocks/sdk/instrumentedFetch`'s `resolveOperation`.
5
+ * SFCC's OCAPI/SCAPI surface is wide; naming by the first meaningful path
6
+ * segment keeps the histogram label low-cardinality without hand-listing every
7
+ * endpoint. Returns `undefined` when nothing meaningful is found, so the
8
+ * framework falls back to `salesforce.fetch`.
9
+ */
10
+
11
+ import { extractPathname } from "@decocms/blocks/sdk/urlUtils";
12
+
13
+ export function salesforceOperationRouter(url: string, _method: string): string | undefined {
14
+ const pathname = extractPathname(url);
15
+
16
+ // SCAPI: /shopper/<api>/v1/... ; OCAPI: /s/<site>/dw/shop/<version>/<resource>/...
17
+ const scapi = pathname.match(/\/shopper\/([^/]+)/);
18
+ if (scapi) return `scapi.${scapi[1]}`;
19
+ const ocapi = pathname.match(/\/dw\/(?:shop|data)\/[^/]+\/([^/?#]+)/);
20
+ if (ocapi) return `ocapi.${ocapi[1]}`;
21
+
22
+ // Fallback: first non-empty path segment.
23
+ const seg = pathname.split("/").find(Boolean);
24
+ return seg ? `path.${seg}` : undefined;
25
+ }