@apifuse/provider-sdk 2.1.0-beta.3 → 2.1.0-beta.5
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/AUTHORING.md +187 -8
- package/CHANGELOG.md +13 -1
- package/README.md +40 -18
- package/SUBMISSION.md +4 -4
- package/bin/apifuse-dev.ts +12 -5
- package/bin/apifuse-pack-check.ts +9 -2
- package/bin/apifuse-pack-smoke.ts +127 -6
- package/bin/apifuse-perf.ts +76 -31
- package/bin/apifuse-record.ts +148 -94
- package/bin/apifuse-submit-check.ts +243 -7
- package/bin/apifuse.ts +1 -1
- package/package.json +17 -8
- package/src/choice-token.ts +164 -0
- package/src/cli/commands.ts +4 -7
- package/src/cli/create.ts +180 -51
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +42 -7
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -47
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
- package/src/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1206 -9
- package/src/define.ts +1620 -106
- package/src/errors.ts +12 -0
- package/src/i18n/catalog.ts +121 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +149 -8
- package/src/lint.ts +306 -51
- package/src/observability.ts +41 -0
- package/src/provider.ts +60 -3
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +77 -21
- package/src/runtime/cache.ts +582 -0
- package/src/runtime/executor.ts +13 -1
- package/src/runtime/http.ts +939 -195
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +76 -0
- package/src/runtime/stealth.ts +1145 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +816 -58
- package/src/server/types.ts +35 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +17 -4
- package/src/types.ts +876 -53
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
|
@@ -39,17 +39,19 @@ const PING_RESPONSE_SCHEMA = z.object({
|
|
|
39
39
|
error: z.unknown().optional(),
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
const KEEP_TEMP = process.env.
|
|
42
|
+
const KEEP_TEMP = process.env.APIFUSE__PACK_SMOKE__KEEP_TEMP === "1";
|
|
43
43
|
|
|
44
44
|
const tempRoot = mkdtempSync(
|
|
45
45
|
join(tmpdir(), "apifuse-provider-sdk-pack-smoke-"),
|
|
46
46
|
);
|
|
47
47
|
const packDir = join(tempRoot, "pack");
|
|
48
48
|
const consumerDir = join(tempRoot, "consumer");
|
|
49
|
+
const externalWorkspaceDir = join(tempRoot, "external-workspace");
|
|
49
50
|
|
|
50
51
|
try {
|
|
51
52
|
mkdirSync(packDir, { recursive: true });
|
|
52
53
|
mkdirSync(consumerDir, { recursive: true });
|
|
54
|
+
mkdirSync(join(externalWorkspaceDir, "providers"), { recursive: true });
|
|
53
55
|
|
|
54
56
|
const packed = packSdk(packDir);
|
|
55
57
|
const tarballPath = resolve(packDir, packed.filename);
|
|
@@ -97,6 +99,11 @@ try {
|
|
|
97
99
|
run("bun", ["run", "test"], generatedProviderDir);
|
|
98
100
|
assertGeneratedReadme(generatedProviderDir);
|
|
99
101
|
await smokeGeneratedDevServer(generatedProviderDir);
|
|
102
|
+
assertExternalWorkspaceTopology(
|
|
103
|
+
cliBin,
|
|
104
|
+
externalWorkspaceDir,
|
|
105
|
+
tarballSpecifier,
|
|
106
|
+
);
|
|
100
107
|
|
|
101
108
|
console.log(
|
|
102
109
|
`Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
|
|
@@ -109,6 +116,103 @@ try {
|
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
function assertExternalWorkspaceTopology(
|
|
120
|
+
cliBin: string,
|
|
121
|
+
externalWorkspaceDir: string,
|
|
122
|
+
tarballSpecifier: string,
|
|
123
|
+
): void {
|
|
124
|
+
writeFileSync(
|
|
125
|
+
join(externalWorkspaceDir, "package.json"),
|
|
126
|
+
`${JSON.stringify(
|
|
127
|
+
{
|
|
128
|
+
private: true,
|
|
129
|
+
type: "module",
|
|
130
|
+
workspaces: ["providers/*"],
|
|
131
|
+
},
|
|
132
|
+
null,
|
|
133
|
+
2,
|
|
134
|
+
)}\n`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
run(
|
|
138
|
+
"bun",
|
|
139
|
+
[
|
|
140
|
+
cliBin,
|
|
141
|
+
"create",
|
|
142
|
+
"external-workspace-smoke",
|
|
143
|
+
"--yes",
|
|
144
|
+
"--json",
|
|
145
|
+
"--sdk-specifier",
|
|
146
|
+
tarballSpecifier,
|
|
147
|
+
],
|
|
148
|
+
externalWorkspaceDir,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const generatedProviderDir = join(
|
|
152
|
+
externalWorkspaceDir,
|
|
153
|
+
"external-workspace-smoke",
|
|
154
|
+
);
|
|
155
|
+
const forbiddenProviderDir = join(
|
|
156
|
+
externalWorkspaceDir,
|
|
157
|
+
"providers",
|
|
158
|
+
"external-workspace-smoke",
|
|
159
|
+
);
|
|
160
|
+
if (!existsSync(generatedProviderDir)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Public create must generate a one-provider repository at <name>/ even when providers/ exists.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (existsSync(forbiddenProviderDir)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
"Public create must not generate providers/<name>/ in external bounty workspaces.",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const packageJson = JSON.parse(
|
|
172
|
+
readFileSync(join(generatedProviderDir, "package.json"), "utf8"),
|
|
173
|
+
);
|
|
174
|
+
const sdkDependency = packageJson?.dependencies?.["@apifuse/provider-sdk"];
|
|
175
|
+
if (sdkDependency !== tarballSpecifier) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Expected generated provider to depend on packed SDK ${tarballSpecifier}, got ${sdkDependency}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (JSON.stringify(packageJson).includes("workspace:")) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"External bounty workspace scaffold must not contain workspace: dependencies.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
run("bun", ["install"], generatedProviderDir);
|
|
187
|
+
run("bun", ["run", "check"], generatedProviderDir);
|
|
188
|
+
run("bun", ["run", "submit-check"], generatedProviderDir);
|
|
189
|
+
run("bun", ["run", "test"], generatedProviderDir);
|
|
190
|
+
|
|
191
|
+
const monorepoAttempt = spawnSync(
|
|
192
|
+
"bun",
|
|
193
|
+
[cliBin, "create", "bad-monorepo-smoke", "--preset", "monorepo", "--yes"],
|
|
194
|
+
{
|
|
195
|
+
cwd: externalWorkspaceDir,
|
|
196
|
+
env: { ...process.env, APIFUSE__SDK__SPECIFIER: tarballSpecifier },
|
|
197
|
+
encoding: "utf8",
|
|
198
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
if (monorepoAttempt.status === 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"--preset monorepo must reject outside the private APIFuse monorepo.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const rejectionOutput = `${monorepoAttempt.stdout}\n${monorepoAttempt.stderr}`;
|
|
207
|
+
if (
|
|
208
|
+
!rejectionOutput.includes(
|
|
209
|
+
"Monorepo preset is internal to the APIFuse repository",
|
|
210
|
+
)
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(`Unexpected monorepo rejection output: ${rejectionOutput}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
112
216
|
function packSdk(destination: string): { filename: string } {
|
|
113
217
|
const raw = execFileSync(
|
|
114
218
|
"npm",
|
|
@@ -162,9 +266,9 @@ function assertGeneratedReadme(providerDir: string): void {
|
|
|
162
266
|
"Generated README is missing browser runtime troubleshooting guidance.",
|
|
163
267
|
);
|
|
164
268
|
}
|
|
165
|
-
if (!readme.includes("
|
|
269
|
+
if (!readme.includes("impit")) {
|
|
166
270
|
throw new Error(
|
|
167
|
-
"Generated README is missing
|
|
271
|
+
"Generated README is missing impit stealth runtime guidance.",
|
|
168
272
|
);
|
|
169
273
|
}
|
|
170
274
|
if (!readme.includes("bun run submit-check")) {
|
|
@@ -183,7 +287,8 @@ async function smokeGeneratedDevServer(providerDir: string): Promise<void> {
|
|
|
183
287
|
const port = await getAvailablePort();
|
|
184
288
|
const server = spawn("bun", ["run", "dev"], {
|
|
185
289
|
cwd: providerDir,
|
|
186
|
-
env: { ...process.env,
|
|
290
|
+
env: { ...process.env, APIFUSE__RUNTIME__PORT: String(port) },
|
|
291
|
+
detached: process.platform !== "win32",
|
|
187
292
|
stdio: ["ignore", "pipe", "pipe"],
|
|
188
293
|
});
|
|
189
294
|
let output = "";
|
|
@@ -286,11 +391,11 @@ async function stopServer(server: ChildProcess): Promise<void> {
|
|
|
286
391
|
if (server.exitCode !== null) {
|
|
287
392
|
return;
|
|
288
393
|
}
|
|
289
|
-
server
|
|
394
|
+
killProcessTree(server, "SIGTERM");
|
|
290
395
|
await new Promise<void>((resolvePromise) => {
|
|
291
396
|
const timeout = setTimeout(() => {
|
|
292
397
|
if (server.exitCode === null) {
|
|
293
|
-
server
|
|
398
|
+
killProcessTree(server, "SIGKILL");
|
|
294
399
|
}
|
|
295
400
|
resolvePromise();
|
|
296
401
|
}, 2_000);
|
|
@@ -300,3 +405,19 @@ async function stopServer(server: ChildProcess): Promise<void> {
|
|
|
300
405
|
});
|
|
301
406
|
});
|
|
302
407
|
}
|
|
408
|
+
|
|
409
|
+
function killProcessTree(server: ChildProcess, signal: NodeJS.Signals): void {
|
|
410
|
+
if (server.pid === undefined) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
if (process.platform === "win32") {
|
|
416
|
+
server.kill(signal);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
process.kill(-server.pid, signal);
|
|
420
|
+
} catch {
|
|
421
|
+
server.kill(signal);
|
|
422
|
+
}
|
|
423
|
+
}
|
package/bin/apifuse-perf.ts
CHANGED
|
@@ -8,8 +8,10 @@ import { pathToFileURL } from "node:url";
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
type ApiFuseConfig,
|
|
11
|
+
createBypassProviderCache,
|
|
11
12
|
createHttpClient,
|
|
12
|
-
|
|
13
|
+
createStealthClient,
|
|
14
|
+
createSttClientFromEnv,
|
|
13
15
|
executeOperation,
|
|
14
16
|
getProviderBaseUrl,
|
|
15
17
|
type HttpClient,
|
|
@@ -18,8 +20,8 @@ import {
|
|
|
18
20
|
type ProviderDefinition,
|
|
19
21
|
ProviderError,
|
|
20
22
|
type Span,
|
|
21
|
-
type
|
|
22
|
-
type
|
|
23
|
+
type StealthClient,
|
|
24
|
+
type StealthResponse,
|
|
23
25
|
wrapWithInstrumentation,
|
|
24
26
|
} from "../src";
|
|
25
27
|
import {
|
|
@@ -41,6 +43,7 @@ type CliArgs = {
|
|
|
41
43
|
exportPath?: string;
|
|
42
44
|
flame: boolean;
|
|
43
45
|
operation: string;
|
|
46
|
+
params?: string;
|
|
44
47
|
runs: number;
|
|
45
48
|
warmup: number;
|
|
46
49
|
};
|
|
@@ -79,6 +82,21 @@ const DEFAULT_RUNS = 10;
|
|
|
79
82
|
const DEFAULT_WARMUP = 2;
|
|
80
83
|
const DEFAULT_CONCURRENCY = 1;
|
|
81
84
|
const BAR_WIDTH = 20;
|
|
85
|
+
const HELP_TEXT = `Usage: apifuse perf <provider-path> --operation <operation> [options]
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--operation, -o <name> operation to profile (required)
|
|
89
|
+
--params, -p <json> JSON input template; falls back to fixtures.request or {}
|
|
90
|
+
--runs, -n <number> number of runs (default: 10)
|
|
91
|
+
--warmup <number> warmup runs (default: 2)
|
|
92
|
+
--concurrency, -c <n> concurrent requests (default: 1)
|
|
93
|
+
--compare-proxy run with proxy on/off and compare
|
|
94
|
+
--export <path> export results to JSON file
|
|
95
|
+
--flame generate flamegraph SVG
|
|
96
|
+
--help, -h show this help
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
apifuse perf providers/airkorea --operation realtime --params '{"stationName":"jongno"}' --runs 5`;
|
|
82
100
|
|
|
83
101
|
export async function main() {
|
|
84
102
|
try {
|
|
@@ -92,7 +110,11 @@ export async function main() {
|
|
|
92
110
|
const inputSchema = getOperationSchema(provider, operation, "input");
|
|
93
111
|
const outputSchema = getOperationSchema(provider, operation, "output");
|
|
94
112
|
const fixtureReplay = await loadFixtureReplay(providerDirectory);
|
|
95
|
-
const inputTemplate = resolveInputTemplate(
|
|
113
|
+
const inputTemplate = resolveInputTemplate(
|
|
114
|
+
provider,
|
|
115
|
+
inputSchema,
|
|
116
|
+
args.params,
|
|
117
|
+
);
|
|
96
118
|
|
|
97
119
|
const directSuite = await runProfileSuite({
|
|
98
120
|
args,
|
|
@@ -182,6 +204,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
182
204
|
let compareProxy = false;
|
|
183
205
|
let exportPath: string | undefined;
|
|
184
206
|
let flame = false;
|
|
207
|
+
let params: string | undefined;
|
|
185
208
|
|
|
186
209
|
for (let index = 0; index < argv.length; index += 1) {
|
|
187
210
|
const arg = argv[index];
|
|
@@ -199,6 +222,11 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
199
222
|
continue;
|
|
200
223
|
}
|
|
201
224
|
|
|
225
|
+
if (arg === "--help" || arg === "-h") {
|
|
226
|
+
console.log(HELP_TEXT);
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
|
|
202
230
|
if (arg === "--compare-proxy") {
|
|
203
231
|
compareProxy = true;
|
|
204
232
|
continue;
|
|
@@ -220,6 +248,22 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
220
248
|
continue;
|
|
221
249
|
}
|
|
222
250
|
|
|
251
|
+
if (arg === "--params" || arg === "-p") {
|
|
252
|
+
params = requireArgValue(argv, index, arg);
|
|
253
|
+
index += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (arg.startsWith("--params=")) {
|
|
258
|
+
params = arg.slice("--params=".length);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (arg.startsWith("-p=")) {
|
|
263
|
+
params = arg.slice("-p=".length);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
223
267
|
if (arg.startsWith("-o=")) {
|
|
224
268
|
operation = arg.slice("-o=".length);
|
|
225
269
|
continue;
|
|
@@ -292,20 +336,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
292
336
|
}
|
|
293
337
|
|
|
294
338
|
if (!providerPath || !operation) {
|
|
295
|
-
throw new Error(
|
|
296
|
-
[
|
|
297
|
-
"Usage: apifuse perf <provider-path> [options]",
|
|
298
|
-
"",
|
|
299
|
-
"Options:",
|
|
300
|
-
" --operation, -o <name> operation to profile (required)",
|
|
301
|
-
" --runs, -n <number> number of runs (default: 10)",
|
|
302
|
-
" --warmup <number> warmup runs (default: 2)",
|
|
303
|
-
" --concurrency, -c <n> concurrent requests (default: 1)",
|
|
304
|
-
" --compare-proxy run with proxy on/off and compare",
|
|
305
|
-
" --export <path> export results to JSON file",
|
|
306
|
-
" --flame generate flamegraph SVG",
|
|
307
|
-
].join("\n"),
|
|
308
|
-
);
|
|
339
|
+
throw new Error(HELP_TEXT);
|
|
309
340
|
}
|
|
310
341
|
|
|
311
342
|
return {
|
|
@@ -315,6 +346,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
315
346
|
exportPath,
|
|
316
347
|
flame,
|
|
317
348
|
operation,
|
|
349
|
+
params,
|
|
318
350
|
runs,
|
|
319
351
|
warmup,
|
|
320
352
|
};
|
|
@@ -409,7 +441,18 @@ function isSchema(value: unknown): value is { parse(input: unknown): unknown } {
|
|
|
409
441
|
function resolveInputTemplate(
|
|
410
442
|
provider: ProviderDefinition,
|
|
411
443
|
inputSchema: { parse(input: unknown): unknown },
|
|
444
|
+
params: string | undefined,
|
|
412
445
|
): unknown {
|
|
446
|
+
if (params !== undefined) {
|
|
447
|
+
try {
|
|
448
|
+
return inputSchema.parse(JSON.parse(params));
|
|
449
|
+
} catch (error) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`Failed to parse --params JSON or validate input: ${error instanceof Error ? error.message : String(error)}`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
413
456
|
const firstOp = Object.values(provider.operations)[0];
|
|
414
457
|
if (firstOp?.fixtures?.request !== undefined) {
|
|
415
458
|
return firstOp.fixtures.request;
|
|
@@ -440,12 +483,12 @@ async function loadFixtureReplay(
|
|
|
440
483
|
}
|
|
441
484
|
|
|
442
485
|
function assertProxyConfigured(config: ApiFuseConfig): void {
|
|
443
|
-
if (config.proxy?.url || process.env.
|
|
486
|
+
if (config.proxy?.url || process.env.APIFUSE__PROXY__URL) {
|
|
444
487
|
return;
|
|
445
488
|
}
|
|
446
489
|
|
|
447
490
|
throw new Error(
|
|
448
|
-
"--compare-proxy requires a proxy URL in apifuse.config.ts or
|
|
491
|
+
"--compare-proxy requires a proxy URL in apifuse.config.ts or APIFUSE__PROXY__URL.",
|
|
449
492
|
);
|
|
450
493
|
}
|
|
451
494
|
|
|
@@ -637,10 +680,10 @@ function createBaseContext(options: {
|
|
|
637
680
|
apifuseConfig,
|
|
638
681
|
upstream,
|
|
639
682
|
});
|
|
640
|
-
const
|
|
683
|
+
const stealth =
|
|
641
684
|
options.forceFixtureReplay && options.fixtureReplay
|
|
642
|
-
?
|
|
643
|
-
:
|
|
685
|
+
? createFixtureStealthClient(options.fixtureReplay.rawText)
|
|
686
|
+
: createStealthClient(getProviderBaseUrl(options.provider), {
|
|
644
687
|
apifuseConfig,
|
|
645
688
|
upstream,
|
|
646
689
|
});
|
|
@@ -657,10 +700,12 @@ function createBaseContext(options: {
|
|
|
657
700
|
getScopes: () => [],
|
|
658
701
|
},
|
|
659
702
|
http,
|
|
660
|
-
|
|
703
|
+
cache: createBypassProviderCache({ providerId: options.provider.id }),
|
|
704
|
+
stealth,
|
|
661
705
|
browser: createBrowserStub(),
|
|
662
706
|
trace: options.traceContext,
|
|
663
707
|
auth: createAuthStub(),
|
|
708
|
+
stt: createSttClientFromEnv(options.provider.stt),
|
|
664
709
|
};
|
|
665
710
|
}
|
|
666
711
|
|
|
@@ -695,8 +740,8 @@ function createFixtureResponse(raw: unknown) {
|
|
|
695
740
|
};
|
|
696
741
|
}
|
|
697
742
|
|
|
698
|
-
function
|
|
699
|
-
const createResponse = async (): Promise<
|
|
743
|
+
function createFixtureStealthClient(rawText: string): StealthClient {
|
|
744
|
+
const createResponse = async (): Promise<StealthResponse> => ({
|
|
700
745
|
status: 200,
|
|
701
746
|
ok: true,
|
|
702
747
|
headers: { "content-type": "application/json" },
|
|
@@ -738,7 +783,7 @@ function buildInsights(
|
|
|
738
783
|
): string[] {
|
|
739
784
|
const insights: string[] = [];
|
|
740
785
|
const allSpans = runs.flatMap((run) => run.spans);
|
|
741
|
-
const
|
|
786
|
+
const stealthSpans = allSpans.filter((span) => span.name === "stealth.fetch");
|
|
742
787
|
const dnsSpans = allSpans.filter((span) => span.name === "dns");
|
|
743
788
|
const transform = breakdown.find(
|
|
744
789
|
(entry) => entry.name === "transformResponse",
|
|
@@ -746,7 +791,7 @@ function buildInsights(
|
|
|
746
791
|
const responseSizes = allSpans
|
|
747
792
|
.map((span) => span.attributes.response_size)
|
|
748
793
|
.filter((value): value is number => typeof value === "number");
|
|
749
|
-
const reuseFlags =
|
|
794
|
+
const reuseFlags = stealthSpans
|
|
750
795
|
.map((span) => span.attributes.connection_reused)
|
|
751
796
|
.filter((value): value is boolean => typeof value === "boolean");
|
|
752
797
|
|
|
@@ -756,8 +801,8 @@ function buildInsights(
|
|
|
756
801
|
);
|
|
757
802
|
insights.push(
|
|
758
803
|
reusePercent >= 80
|
|
759
|
-
? `✓
|
|
760
|
-
: `⚠
|
|
804
|
+
? `✓ Stealth connection reuse: ${reusePercent}% (good)`
|
|
805
|
+
: `⚠ Stealth connection reuse: ${reusePercent}% — consider session reuse`,
|
|
761
806
|
);
|
|
762
807
|
}
|
|
763
808
|
|
|
@@ -1091,7 +1136,7 @@ function cloneValue<T>(value: T): T {
|
|
|
1091
1136
|
|
|
1092
1137
|
function handleCliError(error: unknown): never {
|
|
1093
1138
|
const message = error instanceof Error ? error.message : String(error);
|
|
1094
|
-
console.error(message);
|
|
1139
|
+
console.error(`[apifuse perf] ${message}`);
|
|
1095
1140
|
process.exit(1);
|
|
1096
1141
|
}
|
|
1097
1142
|
|