datadog-ux-utils
Version:
Datadog RUM focused UX & performance toolkit: API guards (retry, breaker, rate), React telemetry (error boundary, profiler, Suspense), web vitals & resource observers, offline queues.
146 lines (145 loc) • 4.7 kB
JavaScript
import { a as h } from "./datadog-irY4zUa4.js";
const i = {
// 0 => use windowMs at runtime
reportDebounceMs: 5e3,
sampleRate: 100,
countOnFailure: !0,
overflowStrategy: "block"
};
class f {
constructor(e) {
this.buckets = /* @__PURE__ */ new Map();
const s = e.keyFn ?? u, t = e.blockDurationMs ?? 0;
this.cfg = {
...e,
keyFn: s,
blockDurationMs: t || e.windowMs,
reportDebounceMs: e.reportDebounceMs ?? i.reportDebounceMs,
sampleRate: e.sampleRate ?? i.sampleRate,
countOnFailure: e.countOnFailure ?? i.countOnFailure,
overflowStrategy: e.overflowStrategy ?? i.overflowStrategy,
allowKey: e.allowKey ?? (() => !0)
};
}
/**
* Wrap a fetch call. If the guard blocks, it throws ApiRunawayBlockedError
* (or queues/drops according to strategy).
*/
async guardFetch(e, s) {
const t = this.cfg.keyFn(e, s);
if (!this.cfg.allowKey(t))
return fetch(e, s);
await this.beforeRequest(t);
let o;
try {
return o = await fetch(e, s), this.afterRequest(t, !0), o;
} catch (a) {
throw this.afterRequest(t, this.cfg.countOnFailure), a;
}
}
/**
* Generic guard for arbitrary async API calls (Axios, graphql-request, etc.)
* Usage:
* await apiGuard.guard('POST /api/items', () => axios.post(...));
*/
async guard(e, s) {
if (!this.cfg.allowKey(e)) return s();
await this.beforeRequest(e);
try {
const t = await s();
return this.afterRequest(e, !0), t;
} catch (t) {
throw this.afterRequest(e, this.cfg.countOnFailure), t;
}
}
/** Clear counters for testing or when navigating away. */
reset(e) {
e ? this.buckets.delete(e) : this.buckets.clear();
}
/* ----------------- internals ----------------- */
getBucket(e) {
let s = this.buckets.get(e);
return s || (s = { hits: [], blockedUntil: 0, lastReportAt: 0 }, this.buckets.set(e, s)), s;
}
async beforeRequest(e) {
const s = Date.now(), t = this.getBucket(e);
if (this.prune(s, t), t.blockedUntil > s)
if (this.cfg.overflowStrategy === "queue")
await this.waitUntil(t.blockedUntil, t);
else {
if (this.cfg.overflowStrategy === "drop")
return this.maybeReport(
e,
s,
t,
/*reason*/
"blocked_active"
), Promise.resolve(void 0);
throw this.maybeReport(e, s, t, "blocked_active"), new n(e, this.cfg, t);
}
if (t.hits.push(s), this.prune(s, t), t.hits.length > this.cfg.maxRequests)
if (t.blockedUntil = s + this.cfg.blockDurationMs, this.maybeReport(e, s, t, "threshold_exceeded"), this.cfg.overflowStrategy === "queue")
t.hits.pop(), await this.waitUntil(t.blockedUntil, t), t.hits.push(Date.now()), this.prune(Date.now(), t);
else {
if (this.cfg.overflowStrategy === "drop")
return t.hits.pop(), Promise.resolve(void 0);
throw t.hits.pop(), new n(e, this.cfg, t);
}
}
afterRequest(e, s) {
s || this.getBucket(e).hits.pop();
}
prune(e, s) {
const t = e - this.cfg.windowMs;
for (; s.hits.length && s.hits[0] < t; ) s.hits.shift();
}
maybeReport(e, s, t, o) {
s - t.lastReportAt < this.cfg.reportDebounceMs || (t.lastReportAt = s, l(this.cfg.sampleRate) && h(
"api_runaway_blocked",
{
// "threshold_exceeded" or "blocked_active"
window_ms: this.cfg.windowMs,
max_requests: this.cfg.maxRequests,
block_ms: this.cfg.blockDurationMs,
count_in_window: t.hits.length
},
this.cfg.sampleRate
));
}
waitUntil(e, s) {
return Date.now() >= e ? Promise.resolve() : (s.waiters || (s.waiters = []), new Promise((t) => {
s.waiters.push(t);
const o = e - Date.now();
setTimeout(() => {
const a = s.waiters;
s.waiters = [], a.forEach((c) => c());
}, o);
}));
}
}
class n extends Error {
constructor(e, s, t) {
super(
`API requests blocked by guard for key "${e}" until ${new Date(
t.blockedUntil
).toISOString()}`
), this.name = "ApiRunawayBlockedError", this.key = e, this.until = t.blockedUntil, this.windowMs = s.windowMs, this.maxRequests = s.maxRequests;
}
}
function u(r, e) {
const s = (e?.method ?? "GET").toUpperCase();
try {
const t = typeof r == "string" ? new URL(r, location.origin) : new URL(r.toString(), location.origin);
return `${s} ${t.pathname}`;
} catch {
return `${s} ${String(r)}`;
}
}
function l(r) {
return Math.random() * 100 < Math.max(0, Math.min(100, Math.round(r)));
}
export {
f as A,
n as a
};
//# sourceMappingURL=rateGuard-DVhJO_z1.js.map