@squasher-ai/browser 0.1.1 → 0.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/LICENSE +21 -21
- package/README.md +3 -42
- package/dist/_vendor/result-runtime/attempt.d.ts +17 -0
- package/dist/_vendor/result-runtime/attempt.js +62 -0
- package/dist/_vendor/sdk-runtime/batching/index.d.ts +42 -0
- package/dist/_vendor/sdk-runtime/batching/index.js +48 -0
- package/dist/_vendor/sdk-runtime/errors/headers.d.ts +6 -0
- package/dist/_vendor/sdk-runtime/errors/headers.js +44 -0
- package/dist/_vendor/sdk-runtime/errors/index.d.ts +95 -0
- package/dist/_vendor/sdk-runtime/errors/index.js +157 -0
- package/dist/_vendor/sdk-runtime/headers.d.ts +48 -0
- package/dist/_vendor/sdk-runtime/headers.js +67 -0
- package/dist/_vendor/sdk-runtime/platform.d.ts +33 -0
- package/dist/_vendor/sdk-runtime/platform.js +173 -0
- package/dist/_vendor/sdk-runtime/retry.d.ts +50 -0
- package/dist/_vendor/sdk-runtime/retry.js +104 -0
- package/dist/_vendor/sdk-runtime/runtime/actionable-error.d.ts +37 -0
- package/dist/_vendor/sdk-runtime/runtime/actionable-error.js +123 -0
- package/dist/_vendor/sdk-runtime/runtime/environment.d.ts +18 -0
- package/dist/_vendor/sdk-runtime/runtime/environment.js +81 -0
- package/dist/_vendor/sdk-runtime/runtime/public-sdk-runtime.d.ts +12 -0
- package/dist/_vendor/sdk-runtime/runtime/public-sdk-runtime.js +38 -0
- package/dist/_vendor/sdk-runtime/runtime/redaction.d.ts +14 -0
- package/dist/_vendor/sdk-runtime/runtime/redaction.js +120 -0
- package/dist/_vendor/sdk-runtime/runtime/release.d.ts +35 -0
- package/dist/_vendor/sdk-runtime/runtime/release.js +120 -0
- package/dist/_vendor/sdk-runtime/sampling/index.d.ts +43 -0
- package/dist/_vendor/sdk-runtime/sampling/index.js +70 -0
- package/dist/_vendor/telemetry-contract/sdk/public-contract.d.ts +175 -0
- package/dist/_vendor/telemetry-contract/sdk/public-contract.js +5 -0
- package/dist/autocapture.d.ts +14 -19
- package/dist/autocapture.js +77 -95
- package/dist/client.d.ts +12 -20
- package/dist/client.js +56 -72
- package/dist/config.d.ts +24 -0
- package/dist/config.js +26 -0
- package/dist/console-capture.d.ts +7 -0
- package/dist/console-capture.js +79 -0
- package/dist/errors.d.ts +2 -3
- package/dist/errors.js +8 -5
- package/dist/index.d.ts +5 -39
- package/dist/index.js +4 -82
- package/dist/next.d.ts +39 -0
- package/dist/next.js +67 -0
- package/dist/react.d.ts +0 -1
- package/dist/react.js +12 -10
- package/dist/replay-network.d.ts +20 -0
- package/dist/replay-network.js +193 -0
- package/dist/replay-privacy.d.ts +2 -3
- package/dist/replay-privacy.js +47 -17
- package/dist/replay-retry.d.ts +40 -0
- package/dist/replay-retry.js +193 -0
- package/dist/replay.d.ts +18 -6
- package/dist/replay.js +131 -71
- package/dist/runtime/client-singleton.d.ts +24 -0
- package/dist/runtime/client-singleton.js +66 -0
- package/dist/runtime/client.d.ts +21 -0
- package/dist/runtime/client.js +12 -0
- package/dist/runtime/replay.d.ts +12 -0
- package/dist/runtime/replay.js +15 -0
- package/dist/session.d.ts +0 -11
- package/dist/session.js +20 -21
- package/dist/telemetry.d.ts +1 -2
- package/dist/telemetry.js +13 -10
- package/dist/trace-context.d.ts +8 -0
- package/dist/trace-context.js +13 -0
- package/dist/transport.d.ts +11 -20
- package/dist/transport.js +86 -66
- package/dist/types.d.ts +38 -139
- package/dist/types.js +0 -7
- package/dist/vitals.d.ts +55 -3
- package/dist/vitals.js +115 -16
- package/package.json +11 -3
- package/dist/__tests__/client.test.d.ts +0 -2
- package/dist/__tests__/client.test.d.ts.map +0 -1
- package/dist/__tests__/client.test.js +0 -103
- package/dist/__tests__/errors.test.d.ts +0 -2
- package/dist/__tests__/errors.test.d.ts.map +0 -1
- package/dist/__tests__/errors.test.js +0 -80
- package/dist/__tests__/replay-privacy.test.d.ts +0 -2
- package/dist/__tests__/replay-privacy.test.d.ts.map +0 -1
- package/dist/__tests__/replay-privacy.test.js +0 -124
- package/dist/__tests__/replay-startup.test.d.ts +0 -2
- package/dist/__tests__/replay-startup.test.d.ts.map +0 -1
- package/dist/__tests__/replay-startup.test.js +0 -99
- package/dist/__tests__/replay.test.d.ts +0 -2
- package/dist/__tests__/replay.test.d.ts.map +0 -1
- package/dist/__tests__/replay.test.js +0 -217
- package/dist/__tests__/session.test.d.ts +0 -2
- package/dist/__tests__/session.test.d.ts.map +0 -1
- package/dist/__tests__/session.test.js +0 -46
- package/dist/__tests__/transport.test.d.ts +0 -2
- package/dist/__tests__/transport.test.d.ts.map +0 -1
- package/dist/__tests__/transport.test.js +0 -101
- package/dist/__tests__/types.test.d.ts +0 -2
- package/dist/__tests__/types.test.d.ts.map +0 -1
- package/dist/__tests__/types.test.js +0 -139
- package/dist/autocapture.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/replay-privacy.d.ts.map +0 -1
- package/dist/replay.d.ts.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/transport.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/vitals.d.ts.map +0 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default telemetry headers attached to every SDK-originated HTTP request.
|
|
3
|
+
*
|
|
4
|
+
* Every request identifies the language, package, package version, OS,
|
|
5
|
+
* architecture, and runtime. Retry count and timeout are added per request.
|
|
6
|
+
*/
|
|
7
|
+
import { getPlatform } from "./platform.js";
|
|
8
|
+
const HEADER_LANG = "X-Squasher-Lang";
|
|
9
|
+
const HEADER_PACKAGE = "X-Squasher-Package";
|
|
10
|
+
const HEADER_PACKAGE_VERSION = "X-Squasher-Package-Version";
|
|
11
|
+
const HEADER_OS = "X-Squasher-OS";
|
|
12
|
+
const HEADER_ARCH = "X-Squasher-Arch";
|
|
13
|
+
const HEADER_RUNTIME = "X-Squasher-Runtime";
|
|
14
|
+
const HEADER_RUNTIME_VERSION = "X-Squasher-Runtime-Version";
|
|
15
|
+
const HEADER_RETRY_COUNT = "X-Squasher-Retry-Count";
|
|
16
|
+
const HEADER_TIMEOUT = "X-Squasher-Timeout";
|
|
17
|
+
/**
|
|
18
|
+
* Build the constant-per-process header set: lang, package, OS, arch, runtime.
|
|
19
|
+
* Cache the return value at the SDK-construction site rather than calling per
|
|
20
|
+
* request — platform detection is cached but header construction still
|
|
21
|
+
* allocates a fresh object.
|
|
22
|
+
*/
|
|
23
|
+
export function getDefaultHeaders(opts) {
|
|
24
|
+
const platform = getPlatform();
|
|
25
|
+
return {
|
|
26
|
+
[HEADER_LANG]: platform.lang,
|
|
27
|
+
[HEADER_PACKAGE]: opts.packageName,
|
|
28
|
+
[HEADER_PACKAGE_VERSION]: opts.packageVersion,
|
|
29
|
+
[HEADER_OS]: platform.os,
|
|
30
|
+
[HEADER_ARCH]: platform.arch,
|
|
31
|
+
[HEADER_RUNTIME]: platform.runtime,
|
|
32
|
+
[HEADER_RUNTIME_VERSION]: platform.runtimeVersion,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the per-request header overlay: retry count and timeout. Returned as
|
|
37
|
+
* a separate object so callers can spread it on top of `getDefaultHeaders()`.
|
|
38
|
+
* Omits any field whose source is undefined so we don't ship empty headers.
|
|
39
|
+
*/
|
|
40
|
+
export function getRequestHeaders(opts) {
|
|
41
|
+
const out = {};
|
|
42
|
+
if (opts.retryCount !== undefined && opts.retryCount > 0) {
|
|
43
|
+
out[HEADER_RETRY_COUNT] = String(opts.retryCount);
|
|
44
|
+
}
|
|
45
|
+
if (opts.timeoutMs !== undefined && opts.timeoutMs > 0) {
|
|
46
|
+
// Express the timeout in seconds with millisecond precision.
|
|
47
|
+
out[HEADER_TIMEOUT] = (opts.timeoutMs / 1000).toString();
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** Build the SDK User-Agent string used by standard HTTP access logs. */
|
|
52
|
+
export function getUserAgent(opts) {
|
|
53
|
+
const { runtime, runtimeVersion, os, arch } = getPlatform();
|
|
54
|
+
return `${opts.packageName}/${opts.packageVersion} (${runtime}/${runtimeVersion}; ${os} ${arch})`;
|
|
55
|
+
}
|
|
56
|
+
/** Header names used by SDK tests and request integrations. */
|
|
57
|
+
export const HEADER_NAMES = {
|
|
58
|
+
lang: HEADER_LANG,
|
|
59
|
+
package: HEADER_PACKAGE,
|
|
60
|
+
packageVersion: HEADER_PACKAGE_VERSION,
|
|
61
|
+
os: HEADER_OS,
|
|
62
|
+
arch: HEADER_ARCH,
|
|
63
|
+
runtime: HEADER_RUNTIME,
|
|
64
|
+
runtimeVersion: HEADER_RUNTIME_VERSION,
|
|
65
|
+
retryCount: HEADER_RETRY_COUNT,
|
|
66
|
+
timeout: HEADER_TIMEOUT,
|
|
67
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime, OS, and architecture detection used by SDK request headers.
|
|
3
|
+
*
|
|
4
|
+
* Probes are wrapped in try/catch because locked-down runtimes can throw when
|
|
5
|
+
* global properties are read. Unknown values remain explicit.
|
|
6
|
+
*/
|
|
7
|
+
export type PlatformRuntime = "node" | "deno" | "bun" | "edge" | "workerd" | `browser:${string}` | "unknown";
|
|
8
|
+
export type PlatformOs = "MacOS" | "Linux" | "Windows" | "FreeBSD" | "OpenBSD" | "iOS" | "Android" | "Unknown";
|
|
9
|
+
export type PlatformArch = "x32" | "x64" | "arm" | "arm64" | "Unknown";
|
|
10
|
+
export interface PlatformProperties {
|
|
11
|
+
/** Language family. Always "js" for any TypeScript/JavaScript SDK. */
|
|
12
|
+
lang: "js";
|
|
13
|
+
/** Concrete runtime identifier. */
|
|
14
|
+
runtime: PlatformRuntime;
|
|
15
|
+
/** Best-effort runtime version string ("20.10.0", "1.0.30", etc.) or "unknown". */
|
|
16
|
+
runtimeVersion: string;
|
|
17
|
+
/** Operating system. */
|
|
18
|
+
os: PlatformOs;
|
|
19
|
+
/** CPU architecture. */
|
|
20
|
+
arch: PlatformArch;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Detect the runtime, OS, arch, and version of the host JS environment.
|
|
24
|
+
*
|
|
25
|
+
* Detection order: Deno → Bun → Cloudflare workerd → generic Edge → Node →
|
|
26
|
+
* Browser → unknown. Earlier checks take precedence because some runtimes
|
|
27
|
+
* (Bun, Deno) also expose a `process` global for compatibility — we want the
|
|
28
|
+
* native identifier to win.
|
|
29
|
+
*/
|
|
30
|
+
export declare function detectPlatform(): PlatformProperties;
|
|
31
|
+
export declare function getPlatform(): PlatformProperties;
|
|
32
|
+
/** Reset cached detection for deterministic tests. */
|
|
33
|
+
export declare function resetPlatformCacheForTesting(): void;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime, OS, and architecture detection used by SDK request headers.
|
|
3
|
+
*
|
|
4
|
+
* Probes are wrapped in try/catch because locked-down runtimes can throw when
|
|
5
|
+
* global properties are read. Unknown values remain explicit.
|
|
6
|
+
*/
|
|
7
|
+
import { readGlobalProperty, readRuntimeProperty, readRuntimeString, } from "./runtime/environment.js";
|
|
8
|
+
function nestedString(value, ...path) {
|
|
9
|
+
let current = value;
|
|
10
|
+
for (const name of path)
|
|
11
|
+
current = readRuntimeProperty(current, name);
|
|
12
|
+
return readRuntimeString(current);
|
|
13
|
+
}
|
|
14
|
+
function normalizeOs(platform) {
|
|
15
|
+
if (!platform)
|
|
16
|
+
return "Unknown";
|
|
17
|
+
if (platform === "darwin")
|
|
18
|
+
return "MacOS";
|
|
19
|
+
if (platform === "linux")
|
|
20
|
+
return "Linux";
|
|
21
|
+
if (platform === "win32")
|
|
22
|
+
return "Windows";
|
|
23
|
+
if (platform === "freebsd")
|
|
24
|
+
return "FreeBSD";
|
|
25
|
+
if (platform === "openbsd")
|
|
26
|
+
return "OpenBSD";
|
|
27
|
+
if (platform === "android")
|
|
28
|
+
return "Android";
|
|
29
|
+
return "Unknown";
|
|
30
|
+
}
|
|
31
|
+
function normalizeArch(arch) {
|
|
32
|
+
if (!arch)
|
|
33
|
+
return "Unknown";
|
|
34
|
+
if (arch === "x32" || arch === "ia32")
|
|
35
|
+
return "x32";
|
|
36
|
+
if (arch === "x64" || arch === "amd64")
|
|
37
|
+
return "x64";
|
|
38
|
+
if (arch === "arm")
|
|
39
|
+
return "arm";
|
|
40
|
+
if (arch === "arm64" || arch === "aarch64")
|
|
41
|
+
return "arm64";
|
|
42
|
+
return "Unknown";
|
|
43
|
+
}
|
|
44
|
+
const BROWSER_UA_PATTERNS = [
|
|
45
|
+
{ key: "edge", re: /Edg(?:e|A|iOS)?\/(\d+(?:\.\d+)?)/ },
|
|
46
|
+
{ key: "ie", re: /MSIE (\d+(?:\.\d+)?)/ },
|
|
47
|
+
{ key: "ie", re: /Trident\/.*; rv:(\d+(?:\.\d+)?)/ },
|
|
48
|
+
{ key: "chrome", re: /Chrome\/(\d+(?:\.\d+)?)/ },
|
|
49
|
+
{ key: "firefox", re: /Firefox\/(\d+(?:\.\d+)?)/ },
|
|
50
|
+
{ key: "safari", re: /Version\/(\d+(?:\.\d+)?).*Safari/ },
|
|
51
|
+
];
|
|
52
|
+
function detectBrowserFromUserAgent(ua) {
|
|
53
|
+
let os = "Unknown";
|
|
54
|
+
if (/Mac OS X/.test(ua))
|
|
55
|
+
os = "MacOS";
|
|
56
|
+
else if (/Windows/.test(ua))
|
|
57
|
+
os = "Windows";
|
|
58
|
+
else if (/Linux/.test(ua))
|
|
59
|
+
os = "Linux";
|
|
60
|
+
else if (/Android/.test(ua))
|
|
61
|
+
os = "Android";
|
|
62
|
+
else if (/iP(hone|ad|od)/.test(ua))
|
|
63
|
+
os = "iOS";
|
|
64
|
+
for (const { key, re } of BROWSER_UA_PATTERNS) {
|
|
65
|
+
const match = re.exec(ua);
|
|
66
|
+
if (match) {
|
|
67
|
+
return { runtime: `browser:${key}`, version: match[1] ?? "unknown", os };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Detect the runtime, OS, arch, and version of the host JS environment.
|
|
74
|
+
*
|
|
75
|
+
* Detection order: Deno → Bun → Cloudflare workerd → generic Edge → Node →
|
|
76
|
+
* Browser → unknown. Earlier checks take precedence because some runtimes
|
|
77
|
+
* (Bun, Deno) also expose a `process` global for compatibility — we want the
|
|
78
|
+
* native identifier to win.
|
|
79
|
+
*/
|
|
80
|
+
export function detectPlatform() {
|
|
81
|
+
// Deno
|
|
82
|
+
const deno = readGlobalProperty("Deno");
|
|
83
|
+
if (deno !== undefined && deno !== null) {
|
|
84
|
+
return {
|
|
85
|
+
lang: "js",
|
|
86
|
+
runtime: "deno",
|
|
87
|
+
runtimeVersion: nestedString(deno, "version", "deno") ?? "unknown",
|
|
88
|
+
os: normalizeOs(nestedString(deno, "build", "os")),
|
|
89
|
+
arch: normalizeArch(nestedString(deno, "build", "arch")),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Bun
|
|
93
|
+
const bun = readGlobalProperty("Bun");
|
|
94
|
+
if (bun !== undefined && bun !== null) {
|
|
95
|
+
const process = readGlobalProperty("process");
|
|
96
|
+
return {
|
|
97
|
+
lang: "js",
|
|
98
|
+
runtime: "bun",
|
|
99
|
+
runtimeVersion: nestedString(bun, "version") ?? "unknown",
|
|
100
|
+
os: normalizeOs(nestedString(process, "platform")),
|
|
101
|
+
arch: normalizeArch(nestedString(process, "arch")),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Cloudflare workerd (preferred over generic edge — gives us version when present)
|
|
105
|
+
const navigatorUserAgent = nestedString(readGlobalProperty("navigator"), "userAgent");
|
|
106
|
+
if (navigatorUserAgent && /Cloudflare-Workers/.test(navigatorUserAgent)) {
|
|
107
|
+
return {
|
|
108
|
+
lang: "js",
|
|
109
|
+
runtime: "workerd",
|
|
110
|
+
runtimeVersion: "unknown",
|
|
111
|
+
os: "Unknown",
|
|
112
|
+
arch: "Unknown",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Generic edge (Vercel Edge, Next.js edge runtime, etc.)
|
|
116
|
+
const edgeRuntime = readGlobalProperty("EdgeRuntime");
|
|
117
|
+
if (edgeRuntime !== undefined) {
|
|
118
|
+
return {
|
|
119
|
+
lang: "js",
|
|
120
|
+
runtime: "edge",
|
|
121
|
+
runtimeVersion: readRuntimeString(edgeRuntime) ?? "unknown",
|
|
122
|
+
os: "Unknown",
|
|
123
|
+
arch: "Unknown",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Node.js — duck-typed via release.name to avoid false positives on
|
|
127
|
+
// bundlers that polyfill `process` for browser code.
|
|
128
|
+
const process = readGlobalProperty("process");
|
|
129
|
+
if (nestedString(process, "release", "name") === "node") {
|
|
130
|
+
return {
|
|
131
|
+
lang: "js",
|
|
132
|
+
runtime: "node",
|
|
133
|
+
runtimeVersion: nestedString(process, "versions", "node") ?? nestedString(process, "version") ?? "unknown",
|
|
134
|
+
os: normalizeOs(nestedString(process, "platform")),
|
|
135
|
+
arch: normalizeArch(nestedString(process, "arch")),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Browser
|
|
139
|
+
if (navigatorUserAgent) {
|
|
140
|
+
const match = detectBrowserFromUserAgent(navigatorUserAgent);
|
|
141
|
+
if (match) {
|
|
142
|
+
return {
|
|
143
|
+
lang: "js",
|
|
144
|
+
runtime: match.runtime,
|
|
145
|
+
runtimeVersion: match.version,
|
|
146
|
+
os: match.os,
|
|
147
|
+
arch: "Unknown",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
lang: "js",
|
|
153
|
+
runtime: "unknown",
|
|
154
|
+
runtimeVersion: "unknown",
|
|
155
|
+
os: "Unknown",
|
|
156
|
+
arch: "Unknown",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Cached platform properties — detection is stable across a process lifetime,
|
|
161
|
+
* so probing once and reusing the result keeps the per-request cost at zero.
|
|
162
|
+
* Tests that need a fresh probe can call `resetPlatformCacheForTesting()`.
|
|
163
|
+
*/
|
|
164
|
+
let cached;
|
|
165
|
+
export function getPlatform() {
|
|
166
|
+
if (!cached)
|
|
167
|
+
cached = detectPlatform();
|
|
168
|
+
return cached;
|
|
169
|
+
}
|
|
170
|
+
/** Reset cached detection for deterministic tests. */
|
|
171
|
+
export function resetPlatformCacheForTesting() {
|
|
172
|
+
cached = undefined;
|
|
173
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry primitives shared across Squasher SDKs.
|
|
3
|
+
*
|
|
4
|
+
* Server retry hints take precedence over exponential backoff with downward
|
|
5
|
+
* jitter. Aborts are never retried; network failures, timeouts, 408, 429, and
|
|
6
|
+
* 5xx responses are retryable by default.
|
|
7
|
+
*/
|
|
8
|
+
import { type HeaderSource } from "./errors/headers.js";
|
|
9
|
+
export interface RetryDecisionInput {
|
|
10
|
+
/** HTTP response status, if a response was received. */
|
|
11
|
+
status?: number;
|
|
12
|
+
/** Response headers — supports either a fetch `Headers` or plain object. */
|
|
13
|
+
headers?: HeaderSource;
|
|
14
|
+
/** Error thrown by `fetch` itself (network failure / abort). */
|
|
15
|
+
thrown?: Error;
|
|
16
|
+
}
|
|
17
|
+
export type HeadersLike = Headers;
|
|
18
|
+
/**
|
|
19
|
+
* Decide whether a response (or thrown error) should trigger a retry.
|
|
20
|
+
*
|
|
21
|
+
* Aborts always opt out — `AbortController.abort()` is user intent, not a
|
|
22
|
+
* transient failure. Connection errors always opt in. Status-code logic
|
|
23
|
+
* mirrors Stainless: 408/409/429 plus any 5xx, with `X-Should-Retry`
|
|
24
|
+
* override.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shouldRetry(input: RetryDecisionInput): boolean;
|
|
27
|
+
export interface BackoffOptions {
|
|
28
|
+
/** Zero-indexed retry attempt number. The first retry uses attempt = 0. */
|
|
29
|
+
attempt: number;
|
|
30
|
+
/** Response headers, used to honor Retry-After / Retry-After-Ms. */
|
|
31
|
+
headers?: HeaderSource;
|
|
32
|
+
/** Random source — injected for deterministic tests. Defaults to Math.random. */
|
|
33
|
+
random?: () => number;
|
|
34
|
+
/** Max sleep in ms. Defaults to 8000 to match Stainless. */
|
|
35
|
+
maxSleepMs?: number;
|
|
36
|
+
/** Reference time used to interpret HTTP-date `Retry-After` values. Tests inject. */
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compute the delay in milliseconds before the next attempt. Server-supplied
|
|
41
|
+
* hints win when present, otherwise falls back to exponential backoff with
|
|
42
|
+
* downward jitter.
|
|
43
|
+
*
|
|
44
|
+
* Returned value is always non-negative. Capped at `maxSleepMs` (default 8s)
|
|
45
|
+
* even when the server requests a longer retry — long sleeps almost always
|
|
46
|
+
* indicate the caller should reach for queueing instead of in-process retries.
|
|
47
|
+
*/
|
|
48
|
+
export declare function nextBackoffMs(opts: BackoffOptions): number;
|
|
49
|
+
/** Promise-friendly setTimeout. Resolves after `ms` milliseconds. */
|
|
50
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry primitives shared across Squasher SDKs.
|
|
3
|
+
*
|
|
4
|
+
* Server retry hints take precedence over exponential backoff with downward
|
|
5
|
+
* jitter. Aborts are never retried; network failures, timeouts, 408, 429, and
|
|
6
|
+
* 5xx responses are retryable by default.
|
|
7
|
+
*/
|
|
8
|
+
import { readHeader } from "./errors/headers.js";
|
|
9
|
+
import { SquasherConnectionError, SquasherUserAbortError } from "./errors/index.js";
|
|
10
|
+
// 408 = Request Timeout, 429 = Too Many Requests. 409 (Conflict) is
|
|
11
|
+
// intentionally NOT included — for Squasher's ingest path, 409 means
|
|
12
|
+
// "duplicate event detected by server dedup", and retrying creates extra
|
|
13
|
+
// writes that re-trip the dedup. Servers that genuinely want a transient
|
|
14
|
+
// 409 retried (e.g. optimistic-locking conflicts) can opt back in via
|
|
15
|
+
// the `X-Should-Retry: true` response header.
|
|
16
|
+
const RETRYABLE_STATUSES = new Set([408, 429]);
|
|
17
|
+
/**
|
|
18
|
+
* Decide whether a response (or thrown error) should trigger a retry.
|
|
19
|
+
*
|
|
20
|
+
* Aborts always opt out — `AbortController.abort()` is user intent, not a
|
|
21
|
+
* transient failure. Connection errors always opt in. Status-code logic
|
|
22
|
+
* mirrors Stainless: 408/409/429 plus any 5xx, with `X-Should-Retry`
|
|
23
|
+
* override.
|
|
24
|
+
*/
|
|
25
|
+
export function shouldRetry(input) {
|
|
26
|
+
if (input.thrown instanceof SquasherUserAbortError)
|
|
27
|
+
return false;
|
|
28
|
+
if (input.thrown instanceof SquasherConnectionError)
|
|
29
|
+
return true;
|
|
30
|
+
if (input.thrown && input.status === undefined) {
|
|
31
|
+
// Unknown thrown value with no response — treat as transport failure.
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const override = readHeader(input.headers, "x-should-retry");
|
|
35
|
+
if (override === "true")
|
|
36
|
+
return true;
|
|
37
|
+
if (override === "false")
|
|
38
|
+
return false;
|
|
39
|
+
if (input.status === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
if (RETRYABLE_STATUSES.has(input.status))
|
|
42
|
+
return true;
|
|
43
|
+
if (input.status >= 500)
|
|
44
|
+
return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const DEFAULT_MAX_SLEEP_MS = 8_000;
|
|
48
|
+
/**
|
|
49
|
+
* Compute the delay in milliseconds before the next attempt. Server-supplied
|
|
50
|
+
* hints win when present, otherwise falls back to exponential backoff with
|
|
51
|
+
* downward jitter.
|
|
52
|
+
*
|
|
53
|
+
* Returned value is always non-negative. Capped at `maxSleepMs` (default 8s)
|
|
54
|
+
* even when the server requests a longer retry — long sleeps almost always
|
|
55
|
+
* indicate the caller should reach for queueing instead of in-process retries.
|
|
56
|
+
*/
|
|
57
|
+
export function nextBackoffMs(opts) {
|
|
58
|
+
const max = opts.maxSleepMs ?? DEFAULT_MAX_SLEEP_MS;
|
|
59
|
+
const random = opts.random ?? Math.random;
|
|
60
|
+
const retryAfterMsHeader = readHeader(opts.headers, "retry-after-ms");
|
|
61
|
+
if (retryAfterMsHeader !== undefined) {
|
|
62
|
+
const ms = Number.parseFloat(retryAfterMsHeader);
|
|
63
|
+
if (Number.isFinite(ms) && ms >= 0)
|
|
64
|
+
return Math.min(ms, max);
|
|
65
|
+
}
|
|
66
|
+
const retryAfterHeader = readHeader(opts.headers, "retry-after");
|
|
67
|
+
if (retryAfterHeader !== undefined) {
|
|
68
|
+
const seconds = Number.parseFloat(retryAfterHeader);
|
|
69
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
70
|
+
return Math.min(seconds * 1000, max);
|
|
71
|
+
}
|
|
72
|
+
// HTTP-date form (RFC 7231)
|
|
73
|
+
const dateMs = Date.parse(retryAfterHeader);
|
|
74
|
+
if (!Number.isNaN(dateMs)) {
|
|
75
|
+
const now = opts.now ? opts.now() : Date.now();
|
|
76
|
+
const delta = dateMs - now;
|
|
77
|
+
if (delta > 0)
|
|
78
|
+
return Math.min(delta, max);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Exponential backoff with downward jitter.
|
|
83
|
+
const baseSeconds = Math.min(0.5 * Math.pow(2, opts.attempt), max / 1000);
|
|
84
|
+
const jitter = 1 - random() * 0.25;
|
|
85
|
+
return Math.min(baseSeconds * 1000 * jitter, max);
|
|
86
|
+
}
|
|
87
|
+
/** Promise-friendly setTimeout. Resolves after `ms` milliseconds. */
|
|
88
|
+
export function sleep(ms, signal) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
if (signal?.aborted) {
|
|
91
|
+
reject(new SquasherUserAbortError());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
signal?.removeEventListener("abort", onAbort);
|
|
96
|
+
resolve();
|
|
97
|
+
}, ms);
|
|
98
|
+
const onAbort = () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
reject(new SquasherUserAbortError());
|
|
101
|
+
};
|
|
102
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SquasherError } from "../errors/index.js";
|
|
2
|
+
export type ActionableErrorValue = boolean | null | number | string;
|
|
3
|
+
export type ActionableErrorInternal = Record<string, ActionableErrorValue | ActionableErrorValue[]>;
|
|
4
|
+
interface ActionableErrorJson extends ActionableErrorContext {
|
|
5
|
+
message: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ActionableErrorContext {
|
|
9
|
+
status?: number | string;
|
|
10
|
+
why?: string;
|
|
11
|
+
fix?: string;
|
|
12
|
+
link?: string;
|
|
13
|
+
internal?: ActionableErrorInternal;
|
|
14
|
+
}
|
|
15
|
+
export interface ActionableErrorOptions extends ActionableErrorContext {
|
|
16
|
+
cause?: Error;
|
|
17
|
+
name?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ActionableErrorTelemetry {
|
|
20
|
+
attributes: Record<string, number | string>;
|
|
21
|
+
extra: {
|
|
22
|
+
squasher_error: ActionableErrorContext;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare class ActionableError extends SquasherError {
|
|
26
|
+
readonly status: number | string | undefined;
|
|
27
|
+
readonly why: string | undefined;
|
|
28
|
+
readonly fix: string | undefined;
|
|
29
|
+
readonly link: string | undefined;
|
|
30
|
+
readonly internal: ActionableErrorInternal | undefined;
|
|
31
|
+
constructor(message: string, options?: ActionableErrorOptions);
|
|
32
|
+
toJSON(): ActionableErrorJson;
|
|
33
|
+
}
|
|
34
|
+
export declare function createActionableError(message: string, options?: ActionableErrorOptions): ActionableError;
|
|
35
|
+
export declare function getActionableErrorContext(error: Error): ActionableErrorContext | undefined;
|
|
36
|
+
export declare function getActionableErrorTelemetry(error: Error): ActionableErrorTelemetry | undefined;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { SquasherError } from "../errors/index.js";
|
|
2
|
+
export class ActionableError extends SquasherError {
|
|
3
|
+
status;
|
|
4
|
+
why;
|
|
5
|
+
fix;
|
|
6
|
+
link;
|
|
7
|
+
constructor(message, options = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = options.name ?? "ActionableError";
|
|
10
|
+
this.status = options.status;
|
|
11
|
+
this.why = options.why;
|
|
12
|
+
this.fix = options.fix;
|
|
13
|
+
this.link = options.link;
|
|
14
|
+
if (options.cause) {
|
|
15
|
+
Object.defineProperty(this, "cause", { value: options.cause });
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(this, "internal", {
|
|
18
|
+
enumerable: false,
|
|
19
|
+
value: readInternal(options.internal),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
toJSON() {
|
|
23
|
+
const result = { message: this.message, name: this.name };
|
|
24
|
+
if (this.status !== undefined)
|
|
25
|
+
result.status = this.status;
|
|
26
|
+
if (this.why)
|
|
27
|
+
result.why = this.why;
|
|
28
|
+
if (this.fix)
|
|
29
|
+
result.fix = this.fix;
|
|
30
|
+
if (this.link)
|
|
31
|
+
result.link = this.link;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function createActionableError(message, options = {}) {
|
|
36
|
+
return new ActionableError(message, options);
|
|
37
|
+
}
|
|
38
|
+
export function getActionableErrorContext(error) {
|
|
39
|
+
const internal = readInternal(readProperty(error, "internal"));
|
|
40
|
+
const context = {
|
|
41
|
+
status: readStatus(readProperty(error, "status")),
|
|
42
|
+
why: readText(readProperty(error, "why")),
|
|
43
|
+
fix: readText(readProperty(error, "fix")),
|
|
44
|
+
link: readText(readProperty(error, "link")),
|
|
45
|
+
internal,
|
|
46
|
+
};
|
|
47
|
+
return context.status !== undefined || context.why || context.fix || context.link || internal
|
|
48
|
+
? context
|
|
49
|
+
: undefined;
|
|
50
|
+
}
|
|
51
|
+
export function getActionableErrorTelemetry(error) {
|
|
52
|
+
const context = getActionableErrorContext(error);
|
|
53
|
+
if (!context)
|
|
54
|
+
return undefined;
|
|
55
|
+
const attributes = {};
|
|
56
|
+
if (context.status !== undefined)
|
|
57
|
+
attributes["error.status"] = context.status;
|
|
58
|
+
if (context.why)
|
|
59
|
+
attributes["error.why"] = context.why;
|
|
60
|
+
if (context.fix)
|
|
61
|
+
attributes["error.fix"] = context.fix;
|
|
62
|
+
if (context.link)
|
|
63
|
+
attributes["error.link"] = context.link;
|
|
64
|
+
return { attributes, extra: { squasher_error: context } };
|
|
65
|
+
}
|
|
66
|
+
function readProperty(error, name) {
|
|
67
|
+
try {
|
|
68
|
+
const owner = Object(error);
|
|
69
|
+
let cursor = owner;
|
|
70
|
+
while (cursor) {
|
|
71
|
+
const descriptor = Object.getOwnPropertyDescriptor(cursor, name);
|
|
72
|
+
if (descriptor) {
|
|
73
|
+
return "value" in descriptor ? descriptor.value : descriptor.get?.call(owner);
|
|
74
|
+
}
|
|
75
|
+
cursor = Object.getPrototypeOf(cursor);
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function readText(value) {
|
|
84
|
+
if (Object.prototype.toString.call(value) !== "[object String]")
|
|
85
|
+
return undefined;
|
|
86
|
+
const text = String(value).trim();
|
|
87
|
+
return text.length > 0 ? text : undefined;
|
|
88
|
+
}
|
|
89
|
+
function readStatus(value) {
|
|
90
|
+
const tag = Object.prototype.toString.call(value);
|
|
91
|
+
if (tag === "[object Number]") {
|
|
92
|
+
const number = Number(value);
|
|
93
|
+
return Number.isFinite(number) ? number : undefined;
|
|
94
|
+
}
|
|
95
|
+
if (tag === "[object String]")
|
|
96
|
+
return readText(value);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
function readInternal(value) {
|
|
100
|
+
if (Object.prototype.toString.call(value) !== "[object Object]")
|
|
101
|
+
return undefined;
|
|
102
|
+
const internal = {};
|
|
103
|
+
for (const [key, entry] of Object.entries(Object(value))) {
|
|
104
|
+
const parsed = readInternalValue(entry);
|
|
105
|
+
if (parsed !== undefined)
|
|
106
|
+
internal[key] = parsed;
|
|
107
|
+
}
|
|
108
|
+
return Object.keys(internal).length > 0 ? internal : undefined;
|
|
109
|
+
}
|
|
110
|
+
function readInternalValue(value) {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
const values = value.filter(isActionableValue);
|
|
113
|
+
return values.length > 0 ? values : undefined;
|
|
114
|
+
}
|
|
115
|
+
return isActionableValue(value) ? value : undefined;
|
|
116
|
+
}
|
|
117
|
+
function isActionableValue(value) {
|
|
118
|
+
const tag = Object.prototype.toString.call(value);
|
|
119
|
+
return (value === null ||
|
|
120
|
+
tag === "[object Boolean]" ||
|
|
121
|
+
tag === "[object Number]" ||
|
|
122
|
+
tag === "[object String]");
|
|
123
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type RuntimePrimitive = boolean | null | number | string;
|
|
2
|
+
export type RuntimeValue = RuntimePrimitive | RuntimeValue[] | RuntimeRecord | undefined;
|
|
3
|
+
export interface RuntimeRecord {
|
|
4
|
+
[key: string]: RuntimeValue;
|
|
5
|
+
}
|
|
6
|
+
export interface RuntimeMethod<TResult> {
|
|
7
|
+
(): TResult;
|
|
8
|
+
}
|
|
9
|
+
export interface RuntimeEnvironment {
|
|
10
|
+
[name: string]: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function readGlobalProperty(name: string): RuntimeValue;
|
|
13
|
+
export declare function readRuntimeProperty<T>(value: T, name: string): RuntimeValue;
|
|
14
|
+
export declare function readRuntimeString<T>(value: T): string | undefined;
|
|
15
|
+
export declare function readRuntimeNumber<T>(value: T): number | undefined;
|
|
16
|
+
export declare function readRuntimeMethod<TResult, T = RuntimeValue>(value: T, name: PropertyKey): RuntimeMethod<TResult> | undefined;
|
|
17
|
+
export declare function readProcessEnvironment(): RuntimeEnvironment;
|
|
18
|
+
export declare function readGlobalEnvironmentValue(name: string): string | undefined;
|