@decocms/apps-magento 7.31.6 → 7.32.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.
- package/package.json +4 -4
- package/src/client.ts +24 -1
- package/src/index.ts +14 -0
- package/src/utils/__tests__/instrumentation.test.ts +59 -0
- package/src/utils/constants.ts +25 -0
- package/src/utils/fetchCache.ts +55 -0
- package/src/utils/instrumentedFetch.ts +57 -0
- package/src/utils/operationRouter.ts +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps-magento",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.32.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco commerce app: Magento integration",
|
|
6
6
|
"repository": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"lint:unused": "knip"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@decocms/blocks": "7.
|
|
30
|
-
"@decocms/apps-commerce": "7.
|
|
31
|
-
"@decocms/tanstack": "7.
|
|
29
|
+
"@decocms/blocks": "7.32.0",
|
|
30
|
+
"@decocms/apps-commerce": "7.32.0",
|
|
31
|
+
"@decocms/tanstack": "7.32.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^19.0.0",
|
package/src/client.ts
CHANGED
|
@@ -88,6 +88,28 @@ export interface MagentoConfig {
|
|
|
88
88
|
|
|
89
89
|
let config: MagentoConfig | null = null;
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Underlying fetch used by {@link magentoFetch}. Defaults to `globalThis.fetch`;
|
|
93
|
+
* override once at boot with {@link setMagentoFetch} to plug in the instrumented
|
|
94
|
+
* fetch (spans + `http.client.request.duration` histogram, `provider:"magento"`).
|
|
95
|
+
* Mirrors VTEX's `setVtexFetch` / Shopify's `setShopifyFetch` so every commerce
|
|
96
|
+
* app funnels egress through a single instrumented `_fetch`.
|
|
97
|
+
*/
|
|
98
|
+
let _fetch: typeof fetch | undefined;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Override the fetch used by every Magento egress call.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { createMagentoFetch, setMagentoFetch } from "@decocms/apps/magento";
|
|
106
|
+
* setMagentoFetch(createMagentoFetch());
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function setMagentoFetch(fetchFn: typeof fetch): void {
|
|
110
|
+
_fetch = fetchFn;
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
export function configureMagento(c: MagentoConfig): void {
|
|
92
114
|
config = c;
|
|
93
115
|
}
|
|
@@ -224,5 +246,6 @@ export function magentoFetch(path: string, opts: MagentoFetchOpts = {}): Promise
|
|
|
224
246
|
// clarity at the call site.
|
|
225
247
|
const sameOrigin = target.origin === baseUrl.origin;
|
|
226
248
|
|
|
227
|
-
|
|
249
|
+
const doFetch = _fetch ?? globalThis.fetch;
|
|
250
|
+
return doFetch(target, { ...opts, headers: buildHeaders(opts, c, sameOrigin) });
|
|
228
251
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,3 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export * from "./client";
|
|
11
11
|
export type { MagentoCart } from "./types";
|
|
12
|
+
export {
|
|
13
|
+
clearFetchCache,
|
|
14
|
+
type FetchCacheOptions,
|
|
15
|
+
getFetchCacheStats,
|
|
16
|
+
magentoCachedFetch,
|
|
17
|
+
} from "./utils/fetchCache";
|
|
18
|
+
// Observability wiring — sites call `setMagentoFetch(createMagentoFetch())` at
|
|
19
|
+
// boot to route every Magento egress call through the instrumented fetch
|
|
20
|
+
// (upstream latency/status), and use `magentoCachedFetch` for cacheable GETs
|
|
21
|
+
// (SWR hit/miss → `deco.cache.requests{layer="swr",profile="magento"}`).
|
|
22
|
+
export {
|
|
23
|
+
type CreateMagentoFetchOptions,
|
|
24
|
+
createMagentoFetch,
|
|
25
|
+
} from "./utils/instrumentedFetch";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage for the Magento observability wiring added so cache/upstream
|
|
3
|
+
* telemetry flows automatically:
|
|
4
|
+
* - `magentoOperationRouter` names REST resources + GraphQL.
|
|
5
|
+
* - `setMagentoFetch` actually reroutes `magentoFetch`'s egress (the hook
|
|
6
|
+
* that lets `createMagentoFetch()`'s instrumentation take effect).
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { magentoOperationRouter } from "../operationRouter";
|
|
10
|
+
|
|
11
|
+
describe("magentoOperationRouter", () => {
|
|
12
|
+
it("names REST resources by their first V1 segment", () => {
|
|
13
|
+
expect(magentoOperationRouter("https://x.com/rest/default/V1/products/42", "GET")).toBe(
|
|
14
|
+
"rest.products",
|
|
15
|
+
);
|
|
16
|
+
expect(magentoOperationRouter("https://x.com/rest/V1/carts/mine", "POST")).toBe("rest.carts");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("names GraphQL calls `graphql`", () => {
|
|
20
|
+
expect(magentoOperationRouter("https://x.com/graphql", "POST")).toBe("graphql");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns undefined when nothing matches (framework falls back)", () => {
|
|
24
|
+
expect(magentoOperationRouter("https://x.com/media/logo.png", "GET")).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("tolerates non-absolute URLs", () => {
|
|
28
|
+
expect(magentoOperationRouter("/rest/default/V1/orders?x=1", "GET")).toBe("rest.orders");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("setMagentoFetch routing", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.resetModules();
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("routes magentoFetch egress through the fetch set via setMagentoFetch", async () => {
|
|
41
|
+
const { configureMagento, setMagentoFetch, magentoFetch } = await import("../../client");
|
|
42
|
+
configureMagento({
|
|
43
|
+
baseUrl: "https://loja.example.com/",
|
|
44
|
+
apiKey: "k",
|
|
45
|
+
storeId: 1,
|
|
46
|
+
site: "example",
|
|
47
|
+
});
|
|
48
|
+
const custom = vi
|
|
49
|
+
.fn()
|
|
50
|
+
.mockResolvedValue(new Response("{}", { status: 200 })) as unknown as typeof fetch;
|
|
51
|
+
const globalSpy = vi.spyOn(globalThis, "fetch");
|
|
52
|
+
setMagentoFetch(custom);
|
|
53
|
+
|
|
54
|
+
await magentoFetch("/rest/default/V1/products/1");
|
|
55
|
+
|
|
56
|
+
expect(custom).toHaveBeenCalledOnce();
|
|
57
|
+
expect(globalSpy).not.toHaveBeenCalled(); // did NOT hit the default fetch
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/utils/constants.ts
CHANGED
|
@@ -12,6 +12,31 @@
|
|
|
12
12
|
|
|
13
13
|
import type { FiltersGraphQL } from "../client";
|
|
14
14
|
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// SWR fetch-cache tuning (consumed by utils/fetchCache.ts via the shared
|
|
17
|
+
// `createFetchCache` in @decocms/blocks/sdk/fetchCache). Mirrors the shape of
|
|
18
|
+
// VTEX's constants so Magento's cache posture is tuned in one place.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Max distinct cache keys kept in memory; oldest `createdAt` evicted first. */
|
|
22
|
+
export const FETCH_CACHE_MAX_ENTRIES = 500;
|
|
23
|
+
|
|
24
|
+
/** How long a cached Magento response stays FRESH, keyed by status class. */
|
|
25
|
+
export const FETCH_CACHE_FRESH_TTL_MS = {
|
|
26
|
+
/** 2xx — 3 min. Catalog/price data doesn't need to be second-fresh. */
|
|
27
|
+
success: 180_000,
|
|
28
|
+
/** 404 — 10s. A just-published product shouldn't 404 for long. */
|
|
29
|
+
notFound: 10_000,
|
|
30
|
+
/** 5xx — never treated as a good cache hit. */
|
|
31
|
+
serverError: 0,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/** Stale-if-error window: serve last-good this long past freshness on outage. */
|
|
35
|
+
export const FETCH_CACHE_STALE_IF_ERROR_MS = 86_400_000; // 24h
|
|
36
|
+
|
|
37
|
+
/** Inflight-slot backstop: bounds how long one hung fetch holds a dedup slot. */
|
|
38
|
+
export const FETCH_CACHE_INFLIGHT_BACKSTOP_MS = 15_000;
|
|
39
|
+
|
|
15
40
|
export const URL_KEY = "url_key";
|
|
16
41
|
|
|
17
42
|
// Schema.org availability mapping (used by utils/transform.ts to
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magento SWR fetch cache — a thin binding over the shared, instrumented
|
|
3
|
+
* `createFetchCache` in `@decocms/blocks/sdk/fetchCache`.
|
|
4
|
+
*
|
|
5
|
+
* Same shared implementation VTEX uses (in-flight dedup, stale-while-
|
|
6
|
+
* revalidate, stale-if-error, inflight backstop), wired with Magento's tuning
|
|
7
|
+
* constants and the `provider: "magento"` label. Every call emits
|
|
8
|
+
* `deco.cache.requests{layer="swr",profile="magento"}` automatically.
|
|
9
|
+
*
|
|
10
|
+
* Cache key is caller-supplied (not required to be a URL): REST GETs key by
|
|
11
|
+
* their URL; GraphQL POSTs can key by a hash of `query + variables`. Callers
|
|
12
|
+
* pass the closure that performs the actual `magentoFetch`, so a HIT never
|
|
13
|
+
* touches the network.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createFetchCache,
|
|
18
|
+
type FetchCacheOptions as SharedFetchCacheOptions,
|
|
19
|
+
} from "@decocms/blocks/sdk/fetchCache";
|
|
20
|
+
import {
|
|
21
|
+
FETCH_CACHE_FRESH_TTL_MS,
|
|
22
|
+
FETCH_CACHE_INFLIGHT_BACKSTOP_MS,
|
|
23
|
+
FETCH_CACHE_MAX_ENTRIES,
|
|
24
|
+
FETCH_CACHE_STALE_IF_ERROR_MS,
|
|
25
|
+
} from "./constants";
|
|
26
|
+
|
|
27
|
+
export type FetchCacheOptions = SharedFetchCacheOptions;
|
|
28
|
+
|
|
29
|
+
const cache = createFetchCache({
|
|
30
|
+
provider: "magento",
|
|
31
|
+
maxEntries: FETCH_CACHE_MAX_ENTRIES,
|
|
32
|
+
freshTtlMs: FETCH_CACHE_FRESH_TTL_MS,
|
|
33
|
+
staleIfErrorMs: FETCH_CACHE_STALE_IF_ERROR_MS,
|
|
34
|
+
inflightBackstopMs: FETCH_CACHE_INFLIGHT_BACKSTOP_MS,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap a Magento GET with SWR caching + in-flight dedup. Returns the parsed
|
|
39
|
+
* JSON body, or `null` for cacheable non-2xx responses (e.g. 404). 5xx throw.
|
|
40
|
+
*/
|
|
41
|
+
export function magentoCachedFetch<T>(
|
|
42
|
+
cacheKey: string,
|
|
43
|
+
doFetch: () => Promise<Response>,
|
|
44
|
+
opts?: FetchCacheOptions,
|
|
45
|
+
): Promise<T | null> {
|
|
46
|
+
return cache.fetchWithCache<T>(cacheKey, doFetch, opts);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearFetchCache() {
|
|
50
|
+
cache.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getFetchCacheStats() {
|
|
54
|
+
return cache.getStats();
|
|
55
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-wired instrumented fetch factory for Magento.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `vtex/utils/instrumentedFetch.ts` and `shopify/utils/instrumentedFetch.ts`.
|
|
5
|
+
* Bundles:
|
|
6
|
+
*
|
|
7
|
+
* 1. `createInstrumentedFetch` from `@decocms/blocks/sdk/instrumentedFetch`
|
|
8
|
+
* (spans, traceparent injection, URL redaction, cache-header span attrs).
|
|
9
|
+
* 2. `magentoOperationRouter` as the URL→operation fallback.
|
|
10
|
+
* 3. An `onComplete` that records the canonical
|
|
11
|
+
* `http.client.request.duration` histogram via the framework's
|
|
12
|
+
* `recordCommerceMetric(...)` helper with `provider: "magento"`.
|
|
13
|
+
*
|
|
14
|
+
* Sites opt in once at startup:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createMagentoFetch, setMagentoFetch } from "@decocms/apps/magento";
|
|
18
|
+
* setMagentoFetch(createMagentoFetch());
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* With this wired, every Magento egress call (GraphQL + REST) funnels through
|
|
22
|
+
* one instrumented boundary, so upstream latency/status lands in ClickHouse.
|
|
23
|
+
* SWR hit/miss for cached GETs is emitted separately by
|
|
24
|
+
* `@decocms/blocks/sdk/fetchCache` (see `./fetchCache.ts`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
createInstrumentedFetch,
|
|
29
|
+
type InstrumentedFetch,
|
|
30
|
+
} from "@decocms/blocks/sdk/instrumentedFetch";
|
|
31
|
+
import { recordCommerceMetric, statusClassFor } from "@decocms/blocks/sdk/observability";
|
|
32
|
+
import { magentoOperationRouter } from "./operationRouter";
|
|
33
|
+
|
|
34
|
+
export interface CreateMagentoFetchOptions {
|
|
35
|
+
/** Underlying fetch to wrap. Defaults to `globalThis.fetch`. */
|
|
36
|
+
baseFetch?: typeof fetch;
|
|
37
|
+
/**
|
|
38
|
+
* Disable the `http.client.request.duration` histogram for Magento calls.
|
|
39
|
+
* Spans + structured logs still emit. Default: false.
|
|
40
|
+
*/
|
|
41
|
+
disableHistogram?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Construct a pre-wired Magento `InstrumentedFetch`. Pass the result to
|
|
46
|
+
* `setMagentoFetch(...)`.
|
|
47
|
+
*/
|
|
48
|
+
export function createMagentoFetch(options: CreateMagentoFetchOptions = {}): InstrumentedFetch {
|
|
49
|
+
const { baseFetch, disableHistogram = false } = options;
|
|
50
|
+
return createInstrumentedFetch({
|
|
51
|
+
name: "magento",
|
|
52
|
+
baseFetch,
|
|
53
|
+
resolveOperation: magentoOperationRouter,
|
|
54
|
+
onComplete: disableHistogram ? undefined : (r) =>
|
|
55
|
+
recordCommerceMetric(r.durationMs, { provider: "magento", operation: r.operation, status_class: statusClassFor(r.status), cached: r.cached }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL-derived operation name router for Magento API calls.
|
|
3
|
+
*
|
|
4
|
+
* Plugged into `@decocms/blocks/sdk/instrumentedFetch`'s `resolveOperation`
|
|
5
|
+
* option. Mirrors the VTEX/Shopify routers. Magento's surface from this repo is
|
|
6
|
+
* a mix of GraphQL (`/graphql`) and REST (`/rest/<store>/V1/...`); the URL alone
|
|
7
|
+
* can name the REST resource, while GraphQL calls fall back to a single
|
|
8
|
+
* `graphql` operation (the semantic name lives in the document body, which
|
|
9
|
+
* callers may stamp as `init.operation` to override this router).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type OperationResolver = string | ((match: RegExpMatchArray, method: string) => string);
|
|
13
|
+
|
|
14
|
+
interface Matcher {
|
|
15
|
+
pattern: RegExp;
|
|
16
|
+
operation: OperationResolver;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const m = (pattern: RegExp, operation: OperationResolver): Matcher => ({ pattern, operation });
|
|
20
|
+
|
|
21
|
+
const MATCHERS: ReadonlyArray<Matcher> = [
|
|
22
|
+
m(/\/graphql\b/, "graphql"),
|
|
23
|
+
// REST: /rest/<storeCode>/V1/<resource>/... — name by the first resource segment.
|
|
24
|
+
m(/\/rest\/[^/]+\/V1\/([^/?#]+)/, (match) => `rest.${match[1]}`),
|
|
25
|
+
m(/\/rest\/V1\/([^/?#]+)/, (match) => `rest.${match[1]}`),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve an operation name for a Magento URL. Returns `undefined` when no
|
|
30
|
+
* matcher fires, so the framework falls back to `magento.fetch`.
|
|
31
|
+
*/
|
|
32
|
+
import { extractPathname } from "@decocms/blocks/sdk/urlUtils";
|
|
33
|
+
|
|
34
|
+
export function magentoOperationRouter(url: string, _method: string): string | undefined {
|
|
35
|
+
const pathname = extractPathname(url);
|
|
36
|
+
|
|
37
|
+
for (const { pattern, operation } of MATCHERS) {
|
|
38
|
+
const match = pathname.match(pattern);
|
|
39
|
+
if (!match) continue;
|
|
40
|
+
return typeof operation === "function" ? operation(match, _method) : operation;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|