@decocms/start 6.2.1 → 6.3.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
CHANGED
|
@@ -253,6 +253,22 @@ export const MetricNames = {
|
|
|
253
253
|
* `MIGRATION_TOOLING_PLAN.md` for the rationale.
|
|
254
254
|
*/
|
|
255
255
|
COMMERCE_REQUEST_DURATION_MS: "commerce_request_duration_ms",
|
|
256
|
+
/**
|
|
257
|
+
* Per-loader execution duration. Emitted by `cachedLoader` for every
|
|
258
|
+
* loader call — cached or not. The `cache_status` label lets dashboards
|
|
259
|
+
* separate origin latency from in-memory hit latency without needing
|
|
260
|
+
* to join on traces.
|
|
261
|
+
*
|
|
262
|
+
* Canonical labels: `loader`, `cache_status`.
|
|
263
|
+
*/
|
|
264
|
+
LOADER_DURATION_MS: "loader_duration_ms",
|
|
265
|
+
/**
|
|
266
|
+
* Counter incremented when a loader throws. Complements
|
|
267
|
+
* `loader_duration_ms` for error-rate dashboards.
|
|
268
|
+
*
|
|
269
|
+
* Canonical labels: `loader`.
|
|
270
|
+
*/
|
|
271
|
+
LOADER_ERRORS_TOTAL: "loader_errors_total",
|
|
256
272
|
} as const;
|
|
257
273
|
|
|
258
274
|
/**
|
|
@@ -455,6 +471,35 @@ export function recordCommerceMetric(
|
|
|
455
471
|
m.histogramRecord?.(MetricNames.COMMERCE_REQUEST_DURATION_MS, durationMs, merged);
|
|
456
472
|
}
|
|
457
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Record a loader execution sample. Call from `cachedLoader` after the
|
|
476
|
+
* loader resolves or rejects. `cache_status` mirrors `CacheDecision` so
|
|
477
|
+
* dashboards can distinguish HIT (fresh) from STALE-HIT (SWR), STALE-ERROR
|
|
478
|
+
* (SIE fallback), MISS (origin fetch), and BYPASS (dev / no-store).
|
|
479
|
+
*/
|
|
480
|
+
export function recordLoaderMetric(
|
|
481
|
+
name: string,
|
|
482
|
+
durationMs: number,
|
|
483
|
+
cacheStatus: CacheDecision | "BYPASS",
|
|
484
|
+
) {
|
|
485
|
+
const m = getState().meter;
|
|
486
|
+
if (!m) return;
|
|
487
|
+
m.histogramRecord?.(MetricNames.LOADER_DURATION_MS, durationMs, {
|
|
488
|
+
loader: name,
|
|
489
|
+
cache_status: cacheStatus,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Increment the loader error counter. Call when a loader throws and the
|
|
495
|
+
* error is not swallowed by a SIE fallback.
|
|
496
|
+
*/
|
|
497
|
+
export function recordLoaderError(name: string) {
|
|
498
|
+
const m = getState().meter;
|
|
499
|
+
if (!m) return;
|
|
500
|
+
m.counterInc(MetricNames.LOADER_ERRORS_TOTAL, 1, { loader: name });
|
|
501
|
+
}
|
|
502
|
+
|
|
458
503
|
function normalizePath(path: string): string {
|
|
459
504
|
// Collapse dynamic segments to reduce cardinality
|
|
460
505
|
return path
|
package/src/sdk/cachedLoader.ts
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* (e.g. "product") which derives timing from the unified profile system.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
recordCacheMetric,
|
|
16
|
+
recordLoaderError,
|
|
17
|
+
recordLoaderMetric,
|
|
18
|
+
withTracing,
|
|
19
|
+
} from "../middleware/observability";
|
|
15
20
|
import { type CacheProfileName, loaderCacheOptions } from "./cacheHeaders";
|
|
16
21
|
|
|
17
22
|
export type CachePolicy = "no-store" | "no-cache" | "stale-while-revalidate";
|
|
@@ -100,17 +105,32 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
100
105
|
if (inflight) {
|
|
101
106
|
// Treat in-flight dedup as a cache hit — avoided the origin call.
|
|
102
107
|
recordCacheMetric(true, name, undefined, "cachedLoader");
|
|
103
|
-
|
|
108
|
+
const start = performance.now();
|
|
109
|
+
return inflight.then((r) => {
|
|
110
|
+
recordLoaderMetric(name, performance.now() - start, "HIT");
|
|
111
|
+
return r as TResult;
|
|
112
|
+
});
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
if (isDev) {
|
|
107
116
|
// Dev mode: no caching, but still useful to count attempts.
|
|
108
117
|
recordCacheMetric(false, name, undefined, "cachedLoader");
|
|
118
|
+
const devStart = performance.now();
|
|
109
119
|
const promise = withTracing(
|
|
110
120
|
"deco.cachedLoader",
|
|
111
|
-
() => loaderFn(props)
|
|
121
|
+
() => loaderFn(props),
|
|
112
122
|
{ "deco.loader": name, "deco.cache.policy": "no-cache-dev" },
|
|
113
|
-
)
|
|
123
|
+
)
|
|
124
|
+
.then((r) => {
|
|
125
|
+
recordLoaderMetric(name, performance.now() - devStart, "BYPASS");
|
|
126
|
+
return r;
|
|
127
|
+
})
|
|
128
|
+
.catch((err) => {
|
|
129
|
+
recordLoaderMetric(name, performance.now() - devStart, "BYPASS");
|
|
130
|
+
recordLoaderError(name);
|
|
131
|
+
throw err;
|
|
132
|
+
})
|
|
133
|
+
.finally(() => inflightRequests.delete(cacheKey));
|
|
114
134
|
inflightRequests.set(cacheKey, promise);
|
|
115
135
|
return promise;
|
|
116
136
|
}
|
|
@@ -122,6 +142,7 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
122
142
|
if (policy === "no-cache") {
|
|
123
143
|
if (entry && !isStale) {
|
|
124
144
|
recordCacheMetric(true, name, "HIT", "cachedLoader");
|
|
145
|
+
recordLoaderMetric(name, 0, "HIT");
|
|
125
146
|
return entry.value;
|
|
126
147
|
}
|
|
127
148
|
}
|
|
@@ -129,12 +150,14 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
129
150
|
if (policy === "stale-while-revalidate") {
|
|
130
151
|
if (entry && !isStale) {
|
|
131
152
|
recordCacheMetric(true, name, "HIT", "cachedLoader");
|
|
153
|
+
recordLoaderMetric(name, 0, "HIT");
|
|
132
154
|
return entry.value;
|
|
133
155
|
}
|
|
134
156
|
|
|
135
157
|
if (entry && isStale && !entry.refreshing) {
|
|
136
158
|
// Stale-while-revalidate hit: serve stale, refresh in background.
|
|
137
159
|
recordCacheMetric(true, name, "STALE-HIT", "cachedLoader");
|
|
160
|
+
recordLoaderMetric(name, 0, "STALE-HIT");
|
|
138
161
|
entry.refreshing = true;
|
|
139
162
|
loaderFn(props)
|
|
140
163
|
.then((result) => {
|
|
@@ -160,6 +183,7 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
160
183
|
// the decision as STALE-ERROR so dashboards can distinguish
|
|
161
184
|
// this from healthy SWR.
|
|
162
185
|
recordCacheMetric(true, name, "STALE-ERROR", "cachedLoader");
|
|
186
|
+
recordLoaderMetric(name, 0, "STALE-ERROR");
|
|
163
187
|
return entry.value;
|
|
164
188
|
}
|
|
165
189
|
}
|
|
@@ -167,11 +191,13 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
167
191
|
// Cache miss — emit metric, then run loader inside a span so individual
|
|
168
192
|
// slow loaders are visible in traces.
|
|
169
193
|
recordCacheMetric(false, name, "MISS", "cachedLoader");
|
|
194
|
+
const loaderStart = performance.now();
|
|
170
195
|
const promise = withTracing("deco.cachedLoader", () => loaderFn(props), {
|
|
171
196
|
"deco.loader": name,
|
|
172
197
|
"deco.cache.policy": policy,
|
|
173
198
|
})
|
|
174
199
|
.then((result) => {
|
|
200
|
+
recordLoaderMetric(name, performance.now() - loaderStart, "MISS");
|
|
175
201
|
cache.set(cacheKey, {
|
|
176
202
|
value: result,
|
|
177
203
|
createdAt: Date.now(),
|
|
@@ -190,9 +216,12 @@ export function createCachedLoader<TProps, TResult>(
|
|
|
190
216
|
console.warn(
|
|
191
217
|
`[cachedLoader] ${name}: origin error, serving stale entry (age=${Math.round(age / 1000)}s, sie=${Math.round(staleIfError / 1000)}s)`,
|
|
192
218
|
);
|
|
219
|
+
recordLoaderMetric(name, performance.now() - loaderStart, "STALE-ERROR");
|
|
193
220
|
return entry.value;
|
|
194
221
|
}
|
|
195
222
|
}
|
|
223
|
+
recordLoaderMetric(name, performance.now() - loaderStart, "MISS");
|
|
224
|
+
recordLoaderError(name);
|
|
196
225
|
throw err;
|
|
197
226
|
});
|
|
198
227
|
|