@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.10
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 +240 -0
- package/CHANGELOG.md +93 -0
- package/README.md +26 -10
- package/SUBMISSION.md +11 -12
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +36 -0
- package/bin/apifuse-pack-smoke.ts +22 -81
- package/bin/apifuse-pack-types.ts +266 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +53 -70
- package/bin/apifuse-submit-check.ts +2177 -353
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +427 -0
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +14 -48
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +47 -33
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/config/loader.d.ts +149 -8
- package/dist/config/loader.js +378 -89
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +5 -6
- package/dist/define.d.ts +13 -1
- package/dist/define.js +245 -178
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +48 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +44 -41
- package/dist/index.js +39 -36
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +35 -15
- package/dist/provider.d.ts +11 -9
- package/dist/provider.js +9 -8
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +4 -2
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +214 -25
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +15 -3
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +160 -344
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +56 -19
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +35 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +1 -1
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +15 -4
- package/dist/runtime/stealth.d.ts +7 -4
- package/dist/runtime/stealth.js +257 -215
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +6 -2
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +199 -0
- package/dist/server/self-test.js +1113 -0
- package/dist/server/serve.d.ts +14 -3
- package/dist/server/serve.js +135 -64
- package/dist/server/types.d.ts +10 -9
- package/dist/server/types.js +3 -7
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +1 -1
- package/dist/testing/run.js +12 -15
- package/dist/types.d.ts +237 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +728 -0
- package/src/ceremonies/index.ts +33 -121
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +69 -99
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- package/src/config/loader.ts +543 -208
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +13 -28
- package/src/define.ts +397 -528
- package/src/dev.ts +4 -9
- package/src/errors.ts +58 -5
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +64 -41
- package/src/lint.ts +122 -159
- package/src/provider.ts +40 -9
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +6 -6
- package/src/runtime/browser.ts +320 -151
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +23 -20
- package/src/runtime/http.ts +216 -539
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +78 -65
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +331 -369
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +44 -3
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +1450 -0
- package/src/server/serve.ts +206 -216
- package/src/server/types.ts +7 -19
- package/src/stealth/profiles.ts +10 -26
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +2 -2
- package/src/testing/run.ts +24 -64
- package/src/types.ts +274 -1
- package/src/user-input.ts +118 -0
package/bin/apifuse-perf.ts
CHANGED
|
@@ -24,19 +24,12 @@ import {
|
|
|
24
24
|
type StealthClient,
|
|
25
25
|
type StealthResponse,
|
|
26
26
|
wrapWithInstrumentation,
|
|
27
|
-
} from "../src";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "../src/
|
|
33
|
-
import { createMemoryProviderRuntimeState } from "../src/runtime/state";
|
|
34
|
-
import {
|
|
35
|
-
createTraceContext,
|
|
36
|
-
resolveTraceContextOptions,
|
|
37
|
-
} from "../src/runtime/trace";
|
|
38
|
-
import { renderWaterfall } from "../src/runtime/waterfall";
|
|
39
|
-
import type { BrowserClient } from "../src/types";
|
|
27
|
+
} from "../src/index.js";
|
|
28
|
+
import { computeStats, groupSpansByName, type PerfStats } from "../src/runtime/perf.js";
|
|
29
|
+
import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
|
|
30
|
+
import { createTraceContext, resolveTraceContextOptions } from "../src/runtime/trace.js";
|
|
31
|
+
import { renderWaterfall } from "../src/runtime/waterfall.js";
|
|
32
|
+
import type { BrowserClient } from "../src/types.js";
|
|
40
33
|
|
|
41
34
|
type CliArgs = {
|
|
42
35
|
compareProxy: boolean;
|
|
@@ -112,11 +105,7 @@ export async function main() {
|
|
|
112
105
|
const inputSchema = getOperationSchema(provider, operation, "input");
|
|
113
106
|
const outputSchema = getOperationSchema(provider, operation, "output");
|
|
114
107
|
const fixtureReplay = await loadFixtureReplay(providerDirectory);
|
|
115
|
-
const inputTemplate = resolveInputTemplate(
|
|
116
|
-
provider,
|
|
117
|
-
inputSchema,
|
|
118
|
-
args.params,
|
|
119
|
-
);
|
|
108
|
+
const inputTemplate = resolveInputTemplate(provider, inputSchema, args.params);
|
|
120
109
|
|
|
121
110
|
const directSuite = await runProfileSuite({
|
|
122
111
|
args,
|
|
@@ -168,9 +157,7 @@ export async function main() {
|
|
|
168
157
|
label: `${providerId}/${args.operation}`,
|
|
169
158
|
proxyEnabled: false,
|
|
170
159
|
stats: directSuite.stats,
|
|
171
|
-
mode: directSuite.runs.some((run) => run.mode === "fixture")
|
|
172
|
-
? "fixture"
|
|
173
|
-
: "live",
|
|
160
|
+
mode: directSuite.runs.some((run) => run.mode === "fixture") ? "fixture" : "live",
|
|
174
161
|
});
|
|
175
162
|
console.log(`Flamegraph: ${flamePath}`);
|
|
176
163
|
}
|
|
@@ -294,27 +281,18 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
294
281
|
}
|
|
295
282
|
|
|
296
283
|
if (arg.startsWith("--warmup=")) {
|
|
297
|
-
warmup = parseNonNegativeInteger(
|
|
298
|
-
arg.slice("--warmup=".length),
|
|
299
|
-
"--warmup",
|
|
300
|
-
);
|
|
284
|
+
warmup = parseNonNegativeInteger(arg.slice("--warmup=".length), "--warmup");
|
|
301
285
|
continue;
|
|
302
286
|
}
|
|
303
287
|
|
|
304
288
|
if (arg === "--concurrency" || arg === "-c") {
|
|
305
|
-
concurrency = parsePositiveInteger(
|
|
306
|
-
requireArgValue(argv, index, arg),
|
|
307
|
-
arg,
|
|
308
|
-
);
|
|
289
|
+
concurrency = parsePositiveInteger(requireArgValue(argv, index, arg), arg);
|
|
309
290
|
index += 1;
|
|
310
291
|
continue;
|
|
311
292
|
}
|
|
312
293
|
|
|
313
294
|
if (arg.startsWith("--concurrency=")) {
|
|
314
|
-
concurrency = parsePositiveInteger(
|
|
315
|
-
arg.slice("--concurrency=".length),
|
|
316
|
-
"--concurrency",
|
|
317
|
-
);
|
|
295
|
+
concurrency = parsePositiveInteger(arg.slice("--concurrency=".length), "--concurrency");
|
|
318
296
|
continue;
|
|
319
297
|
}
|
|
320
298
|
|
|
@@ -354,11 +332,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
354
332
|
};
|
|
355
333
|
}
|
|
356
334
|
|
|
357
|
-
function requireArgValue(
|
|
358
|
-
argv: string[],
|
|
359
|
-
index: number,
|
|
360
|
-
option: string,
|
|
361
|
-
): string {
|
|
335
|
+
function requireArgValue(argv: string[], index: number, option: string): string {
|
|
362
336
|
const value = argv[index + 1];
|
|
363
337
|
if (!value) {
|
|
364
338
|
throw new Error(`Missing value for ${option}`);
|
|
@@ -396,9 +370,7 @@ function resolveProviderEntry(providerDirectory: string): string {
|
|
|
396
370
|
throw new Error(`Provider entry not found in ${providerDirectory}`);
|
|
397
371
|
}
|
|
398
372
|
|
|
399
|
-
async function loadProvider(
|
|
400
|
-
providerEntry: string,
|
|
401
|
-
): Promise<ProviderDefinition> {
|
|
373
|
+
async function loadProvider(providerEntry: string): Promise<ProviderDefinition> {
|
|
402
374
|
const mod = (await import(pathToFileURL(providerEntry).href)) as {
|
|
403
375
|
default?: ProviderDefinition;
|
|
404
376
|
};
|
|
@@ -469,9 +441,7 @@ function resolveInputTemplate(
|
|
|
469
441
|
}
|
|
470
442
|
}
|
|
471
443
|
|
|
472
|
-
async function loadFixtureReplay(
|
|
473
|
-
providerDirectory: string,
|
|
474
|
-
): Promise<FixtureReplay | null> {
|
|
444
|
+
async function loadFixtureReplay(providerDirectory: string): Promise<FixtureReplay | null> {
|
|
475
445
|
const fixturePath = resolve(providerDirectory, "__fixtures__", "raw.json");
|
|
476
446
|
if (!existsSync(fixturePath)) {
|
|
477
447
|
return null;
|
|
@@ -521,15 +491,8 @@ async function runProfileSuite(options: {
|
|
|
521
491
|
}
|
|
522
492
|
|
|
523
493
|
const runs: RunResult[] = [];
|
|
524
|
-
for (
|
|
525
|
-
|
|
526
|
-
index < options.args.runs;
|
|
527
|
-
index += options.args.concurrency
|
|
528
|
-
) {
|
|
529
|
-
const batchSize = Math.min(
|
|
530
|
-
options.args.concurrency,
|
|
531
|
-
options.args.runs - index,
|
|
532
|
-
);
|
|
494
|
+
for (let index = 0; index < options.args.runs; index += options.args.concurrency) {
|
|
495
|
+
const batchSize = Math.min(options.args.concurrency, options.args.runs - index);
|
|
533
496
|
const batch = Array.from({ length: batchSize }, () =>
|
|
534
497
|
profileRun({
|
|
535
498
|
config: options.config,
|
|
@@ -552,8 +515,7 @@ async function runProfileSuite(options: {
|
|
|
552
515
|
const breakdown = [...groupedSpans.entries()]
|
|
553
516
|
.filter(([name]) => name !== rootSpanName)
|
|
554
517
|
.map(([name, durations]) => {
|
|
555
|
-
const avgMs =
|
|
556
|
-
durations.reduce((sum, value) => sum + value, 0) / durations.length;
|
|
518
|
+
const avgMs = durations.reduce((sum, value) => sum + value, 0) / durations.length;
|
|
557
519
|
const percent = stats.avg > 0 ? (avgMs / stats.avg) * 100 : 0;
|
|
558
520
|
return { avgMs, name, percent };
|
|
559
521
|
})
|
|
@@ -611,9 +573,7 @@ async function executeProfileRun(options: {
|
|
|
611
573
|
proxyEnabled: boolean;
|
|
612
574
|
}): Promise<RunResult> {
|
|
613
575
|
const _operation = getOperation(options.provider, options.operationName);
|
|
614
|
-
const traceContext = createTraceContext(
|
|
615
|
-
resolveTraceContextOptions(options.config.trace),
|
|
616
|
-
);
|
|
576
|
+
const traceContext = createTraceContext(resolveTraceContextOptions(options.config.trace));
|
|
617
577
|
const baseContext = createBaseContext({
|
|
618
578
|
config: options.config,
|
|
619
579
|
provider: options.provider,
|
|
@@ -638,9 +598,7 @@ async function executeProfileRun(options: {
|
|
|
638
598
|
normalizedInput,
|
|
639
599
|
);
|
|
640
600
|
|
|
641
|
-
return ctx.trace.span("transformResponse", async () =>
|
|
642
|
-
options.outputSchema.parse(result),
|
|
643
|
-
);
|
|
601
|
+
return ctx.trace.span("transformResponse", async () => options.outputSchema.parse(result));
|
|
644
602
|
});
|
|
645
603
|
const durationMs = performance.now() - startedAt;
|
|
646
604
|
const spans = ctx.trace.getSpans();
|
|
@@ -790,36 +748,24 @@ function createBrowserStub(): BrowserClient {
|
|
|
790
748
|
engine: "playwright-stealth",
|
|
791
749
|
async close() {},
|
|
792
750
|
async newPage() {
|
|
793
|
-
throw new ProviderError(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
797
|
-
},
|
|
798
|
-
);
|
|
751
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
752
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
753
|
+
});
|
|
799
754
|
},
|
|
800
755
|
async rawPage() {
|
|
801
|
-
throw new ProviderError(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
805
|
-
},
|
|
806
|
-
);
|
|
756
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
757
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
758
|
+
});
|
|
807
759
|
},
|
|
808
760
|
async withIsolatedContext() {
|
|
809
|
-
throw new ProviderError(
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
813
|
-
},
|
|
814
|
-
);
|
|
761
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
762
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
763
|
+
});
|
|
815
764
|
},
|
|
816
765
|
async solveChallenge() {
|
|
817
|
-
throw new ProviderError(
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
821
|
-
},
|
|
822
|
-
);
|
|
766
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
767
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
768
|
+
});
|
|
823
769
|
},
|
|
824
770
|
};
|
|
825
771
|
}
|
|
@@ -833,9 +779,7 @@ function buildInsights(
|
|
|
833
779
|
const allSpans = runs.flatMap((run) => run.spans);
|
|
834
780
|
const stealthSpans = allSpans.filter((span) => span.name === "stealth.fetch");
|
|
835
781
|
const dnsSpans = allSpans.filter((span) => span.name === "dns");
|
|
836
|
-
const transform = breakdown.find(
|
|
837
|
-
(entry) => entry.name === "transformResponse",
|
|
838
|
-
);
|
|
782
|
+
const transform = breakdown.find((entry) => entry.name === "transformResponse");
|
|
839
783
|
const responseSizes = allSpans
|
|
840
784
|
.map((span) => span.attributes.response_size)
|
|
841
785
|
.filter((value): value is number => typeof value === "number");
|
|
@@ -844,9 +788,7 @@ function buildInsights(
|
|
|
844
788
|
.filter((value): value is boolean => typeof value === "boolean");
|
|
845
789
|
|
|
846
790
|
if (reuseFlags.length > 0) {
|
|
847
|
-
const reusePercent = Math.round(
|
|
848
|
-
(reuseFlags.filter(Boolean).length / reuseFlags.length) * 100,
|
|
849
|
-
);
|
|
791
|
+
const reusePercent = Math.round((reuseFlags.filter(Boolean).length / reuseFlags.length) * 100);
|
|
850
792
|
insights.push(
|
|
851
793
|
reusePercent >= 80
|
|
852
794
|
? `✓ Stealth connection reuse: ${reusePercent}% (good)`
|
|
@@ -863,24 +805,16 @@ function buildInsights(
|
|
|
863
805
|
}
|
|
864
806
|
|
|
865
807
|
if (dnsSpans.length > 0) {
|
|
866
|
-
const dnsAvg =
|
|
867
|
-
dnsSpans.reduce((sum, span) => sum + span.duration_ms, 0) /
|
|
868
|
-
dnsSpans.length;
|
|
808
|
+
const dnsAvg = dnsSpans.reduce((sum, span) => sum + span.duration_ms, 0) / dnsSpans.length;
|
|
869
809
|
if (dnsAvg >= 5) {
|
|
870
|
-
insights.push(
|
|
871
|
-
`⚠ DNS resolution: ${formatDuration(dnsAvg)} avg — consider DNS caching`,
|
|
872
|
-
);
|
|
810
|
+
insights.push(`⚠ DNS resolution: ${formatDuration(dnsAvg)} avg — consider DNS caching`);
|
|
873
811
|
}
|
|
874
812
|
}
|
|
875
813
|
|
|
876
814
|
if (responseSizes.length > 0) {
|
|
877
|
-
const avgSize =
|
|
878
|
-
responseSizes.reduce((sum, value) => sum + value, 0) /
|
|
879
|
-
responseSizes.length;
|
|
815
|
+
const avgSize = responseSizes.reduce((sum, value) => sum + value, 0) / responseSizes.length;
|
|
880
816
|
if (avgSize >= 4096) {
|
|
881
|
-
insights.push(
|
|
882
|
-
`⚠ Response size: ${formatBytes(avgSize)} avg — check gzip (Accept-Encoding)`,
|
|
883
|
-
);
|
|
817
|
+
insights.push(`⚠ Response size: ${formatBytes(avgSize)} avg — check gzip (Accept-Encoding)`);
|
|
884
818
|
}
|
|
885
819
|
}
|
|
886
820
|
|
|
@@ -925,13 +859,10 @@ function renderReport(options: {
|
|
|
925
859
|
|
|
926
860
|
if (options.proxySuite) {
|
|
927
861
|
const delta = options.proxySuite.stats.p50 - options.suite.stats.p50;
|
|
928
|
-
const deltaPercent =
|
|
929
|
-
options.suite.stats.p50 > 0 ? (delta / options.suite.stats.p50) * 100 : 0;
|
|
862
|
+
const deltaPercent = options.suite.stats.p50 > 0 ? (delta / options.suite.stats.p50) * 100 : 0;
|
|
930
863
|
lines.push("│");
|
|
931
864
|
lines.push("│ Proxy Comparison (--compare-proxy):");
|
|
932
|
-
lines.push(
|
|
933
|
-
`│ proxy: off → p50: ${formatDuration(options.suite.stats.p50)}`,
|
|
934
|
-
);
|
|
865
|
+
lines.push(`│ proxy: off → p50: ${formatDuration(options.suite.stats.p50)}`);
|
|
935
866
|
lines.push(
|
|
936
867
|
`│ proxy: on → p50: ${formatDuration(options.proxySuite.stats.p50)} (${delta >= 0 ? "+" : ""}${Math.round(deltaPercent)}%)`,
|
|
937
868
|
);
|
|
@@ -979,9 +910,7 @@ function formatBytes(value: number): string {
|
|
|
979
910
|
}
|
|
980
911
|
|
|
981
912
|
function selectRepresentativeRun(runs: RunResult[]): RunResult {
|
|
982
|
-
const sorted = [...runs].sort(
|
|
983
|
-
(left, right) => left.durationMs - right.durationMs,
|
|
984
|
-
);
|
|
913
|
+
const sorted = [...runs].sort((left, right) => left.durationMs - right.durationMs);
|
|
985
914
|
const middle = sorted[Math.floor(sorted.length / 2)];
|
|
986
915
|
if (middle) {
|
|
987
916
|
return middle;
|
|
@@ -1065,17 +994,10 @@ function buildFlamegraphSvg(
|
|
|
1065
994
|
|
|
1066
995
|
const rects = flattened
|
|
1067
996
|
.map((node) => {
|
|
1068
|
-
const x =
|
|
1069
|
-
|
|
1070
|
-
((node.span.startedAt - root.startedAt) / totalDuration) * (width - 24);
|
|
1071
|
-
const rectWidth = Math.max(
|
|
1072
|
-
1,
|
|
1073
|
-
(node.span.duration_ms / totalDuration) * (width - 24),
|
|
1074
|
-
);
|
|
997
|
+
const x = 12 + ((node.span.startedAt - root.startedAt) / totalDuration) * (width - 24);
|
|
998
|
+
const rectWidth = Math.max(1, (node.span.duration_ms / totalDuration) * (width - 24));
|
|
1075
999
|
const y = chartTop + node.depth * rowHeight;
|
|
1076
|
-
const label = escapeXml(
|
|
1077
|
-
`${node.span.name} (${formatDuration(node.span.duration_ms)})`,
|
|
1078
|
-
);
|
|
1000
|
+
const label = escapeXml(`${node.span.name} (${formatDuration(node.span.duration_ms)})`);
|
|
1079
1001
|
const title = escapeXml(
|
|
1080
1002
|
`${node.span.name}\n${formatDuration(node.span.duration_ms)}\nstatus: ${node.span.status}`,
|
|
1081
1003
|
);
|
|
@@ -1094,9 +1016,7 @@ function buildFlamegraphSvg(
|
|
|
1094
1016
|
}
|
|
1095
1017
|
|
|
1096
1018
|
function buildFlameTree(spans: Span[]): FlameNode[] {
|
|
1097
|
-
const sorted = [...spans].sort(
|
|
1098
|
-
(left, right) => left.startedAt - right.startedAt,
|
|
1099
|
-
);
|
|
1019
|
+
const sorted = [...spans].sort((left, right) => left.startedAt - right.startedAt);
|
|
1100
1020
|
const nodeMap = new Map<string, FlameNode>();
|
|
1101
1021
|
|
|
1102
1022
|
for (const span of sorted) {
|
|
@@ -1139,9 +1059,7 @@ function findRootSpan(spans: Span[]): Span {
|
|
|
1139
1059
|
return root;
|
|
1140
1060
|
}
|
|
1141
1061
|
|
|
1142
|
-
const sorted = [...spans].sort(
|
|
1143
|
-
(left, right) => left.startedAt - right.startedAt,
|
|
1144
|
-
);
|
|
1062
|
+
const sorted = [...spans].sort((left, right) => left.startedAt - right.startedAt);
|
|
1145
1063
|
const first = sorted[0];
|
|
1146
1064
|
if (!first) {
|
|
1147
1065
|
throw new Error("Expected at least one span");
|
package/bin/apifuse-record.ts
CHANGED
|
@@ -20,8 +20,8 @@ import {
|
|
|
20
20
|
type StealthClient,
|
|
21
21
|
TransportError,
|
|
22
22
|
ValidationError,
|
|
23
|
-
} from "../src";
|
|
24
|
-
import { createMemoryProviderRuntimeState } from "../src/runtime/state";
|
|
23
|
+
} from "../src/index.js";
|
|
24
|
+
import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
|
|
25
25
|
|
|
26
26
|
type CliArgs = {
|
|
27
27
|
append: boolean;
|
|
@@ -42,7 +42,7 @@ Calls a real upstream-backed operation through ctx.http or ctx.stealth and write
|
|
|
42
42
|
Options:
|
|
43
43
|
--operation, -o <name> operation to call
|
|
44
44
|
--params, -p <json> JSON input passed to the operation (default: {})
|
|
45
|
-
--append
|
|
45
|
+
--append preserve the existing fixture and append this capture
|
|
46
46
|
--sanitize redact common token/header fields (default)
|
|
47
47
|
--no-sanitize write the captured upstream payload as-is
|
|
48
48
|
--help, -h show this help
|
|
@@ -64,31 +64,18 @@ export async function main() {
|
|
|
64
64
|
resolveOperationBaseUrl(provider, operationName),
|
|
65
65
|
);
|
|
66
66
|
|
|
67
|
-
console.log(
|
|
68
|
-
`[apifuse record] Calling ${operationName} on ${provider.id}...`,
|
|
69
|
-
);
|
|
67
|
+
console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
|
|
70
68
|
|
|
71
|
-
const result = await executeOperation(
|
|
72
|
-
provider,
|
|
73
|
-
operationName,
|
|
74
|
-
capture.ctx,
|
|
75
|
-
parsedParams,
|
|
76
|
-
);
|
|
69
|
+
const result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
|
|
77
70
|
const captured = capture.getCapturedRaw();
|
|
78
71
|
|
|
79
72
|
if (captured === undefined) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`No upstream response was captured for ${provider.id}.${operationName}.`,
|
|
82
|
-
);
|
|
73
|
+
throw new Error(`No upstream response was captured for ${provider.id}.${operationName}.`);
|
|
83
74
|
}
|
|
84
75
|
|
|
85
76
|
const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
|
|
86
77
|
const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
|
|
87
|
-
const nextPayload = await prepareFixturePayload(
|
|
88
|
-
fixturePath,
|
|
89
|
-
rawPayload,
|
|
90
|
-
args.append,
|
|
91
|
-
);
|
|
78
|
+
const nextPayload = await prepareFixturePayload(fixturePath, rawPayload, args.append);
|
|
92
79
|
|
|
93
80
|
await mkdir(dirname(fixturePath), { recursive: true });
|
|
94
81
|
await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
|
|
@@ -98,9 +85,7 @@ export async function main() {
|
|
|
98
85
|
Buffer.byteLength(JSON.stringify(rawPayload)),
|
|
99
86
|
)})`,
|
|
100
87
|
);
|
|
101
|
-
console.log(
|
|
102
|
-
`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
|
|
103
|
-
);
|
|
88
|
+
console.log(`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`);
|
|
104
89
|
|
|
105
90
|
void result;
|
|
106
91
|
} catch (error) {
|
|
@@ -210,11 +195,7 @@ function formatCliError(error: unknown): string {
|
|
|
210
195
|
}
|
|
211
196
|
|
|
212
197
|
if (error instanceof ProviderError || error instanceof ValidationError) {
|
|
213
|
-
return [
|
|
214
|
-
error.message,
|
|
215
|
-
error.code ? `code=${error.code}` : undefined,
|
|
216
|
-
error.fix,
|
|
217
|
-
]
|
|
198
|
+
return [error.message, error.code ? `code=${error.code}` : undefined, error.fix]
|
|
218
199
|
.filter(Boolean)
|
|
219
200
|
.join(" ");
|
|
220
201
|
}
|
|
@@ -269,8 +250,7 @@ function findProviderRoot(startDirectory: string): string | undefined {
|
|
|
269
250
|
|
|
270
251
|
function looksLikeProviderRoot(directory: string): boolean {
|
|
271
252
|
return (
|
|
272
|
-
existsSync(resolve(directory, "index.ts")) &&
|
|
273
|
-
existsSync(resolve(directory, "package.json"))
|
|
253
|
+
existsSync(resolve(directory, "index.ts")) && existsSync(resolve(directory, "package.json"))
|
|
274
254
|
);
|
|
275
255
|
}
|
|
276
256
|
|
|
@@ -287,15 +267,10 @@ async function loadProvider(rootDir: string): Promise<ProviderRuntime> {
|
|
|
287
267
|
return module.default;
|
|
288
268
|
}
|
|
289
269
|
|
|
290
|
-
function resolveOperationName(
|
|
291
|
-
provider: ProviderRuntime,
|
|
292
|
-
operationName?: string,
|
|
293
|
-
): string {
|
|
270
|
+
function resolveOperationName(provider: ProviderRuntime, operationName?: string): string {
|
|
294
271
|
if (operationName) {
|
|
295
272
|
if (!(operationName in provider.operations)) {
|
|
296
|
-
throw new Error(
|
|
297
|
-
`Unknown operation "${operationName}" for provider "${provider.id}".`,
|
|
298
|
-
);
|
|
273
|
+
throw new Error(`Unknown operation "${operationName}" for provider "${provider.id}".`);
|
|
299
274
|
}
|
|
300
275
|
|
|
301
276
|
return operationName;
|
|
@@ -309,10 +284,7 @@ function resolveOperationName(
|
|
|
309
284
|
return firstOperation;
|
|
310
285
|
}
|
|
311
286
|
|
|
312
|
-
function parseParams(
|
|
313
|
-
operation: ProviderRuntime["operations"][string],
|
|
314
|
-
value: string,
|
|
315
|
-
): unknown {
|
|
287
|
+
function parseParams(operation: ProviderRuntime["operations"][string], value: string): unknown {
|
|
316
288
|
let parsed: unknown;
|
|
317
289
|
|
|
318
290
|
try {
|
|
@@ -326,10 +298,7 @@ function parseParams(
|
|
|
326
298
|
return operation.input ? operation.input.parse(parsed) : parsed;
|
|
327
299
|
}
|
|
328
300
|
|
|
329
|
-
function resolveOperationBaseUrl(
|
|
330
|
-
provider: ProviderRuntime,
|
|
331
|
-
operationName: string,
|
|
332
|
-
): string {
|
|
301
|
+
function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: string): string {
|
|
333
302
|
const baseUrl = provider.operations[operationName]?.upstream?.baseUrl;
|
|
334
303
|
if (!baseUrl) {
|
|
335
304
|
throw new Error(
|
|
@@ -346,12 +315,9 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
|
|
|
346
315
|
const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
|
|
347
316
|
capturedRaw = response.data;
|
|
348
317
|
});
|
|
349
|
-
const stealth = proxyStealthClient(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
capturedRaw = normalizeCapturedStealthResponse(response);
|
|
353
|
-
},
|
|
354
|
-
);
|
|
318
|
+
const stealth = proxyStealthClient(createStealthClient(baseUrl), (response) => {
|
|
319
|
+
capturedRaw = normalizeCapturedStealthResponse(response);
|
|
320
|
+
});
|
|
355
321
|
|
|
356
322
|
const env = {
|
|
357
323
|
get: (key: string) => process.env[key],
|
|
@@ -464,9 +430,7 @@ function proxyStealthSession(
|
|
|
464
430
|
};
|
|
465
431
|
}
|
|
466
432
|
|
|
467
|
-
function normalizeCapturedStealthResponse(
|
|
468
|
-
response: Awaited<ReturnType<StealthClient["fetch"]>>,
|
|
469
|
-
) {
|
|
433
|
+
function normalizeCapturedStealthResponse(response: Awaited<ReturnType<StealthClient["fetch"]>>) {
|
|
470
434
|
try {
|
|
471
435
|
return JSON.parse(response.body);
|
|
472
436
|
} catch {
|
|
@@ -483,15 +447,13 @@ function sanitizeFixture(value: unknown): unknown {
|
|
|
483
447
|
return value;
|
|
484
448
|
}
|
|
485
449
|
|
|
486
|
-
const entries = Object.entries(value as MutableRecord).map(
|
|
487
|
-
(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
450
|
+
const entries = Object.entries(value as MutableRecord).map(([key, entryValue]) => {
|
|
451
|
+
if (isSensitiveKey(key)) {
|
|
452
|
+
return [key, "[REDACTED]"] as const;
|
|
453
|
+
}
|
|
491
454
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
);
|
|
455
|
+
return [key, sanitizeFixture(entryValue)] as const;
|
|
456
|
+
});
|
|
495
457
|
|
|
496
458
|
return Object.fromEntries(entries);
|
|
497
459
|
}
|
|
@@ -500,7 +462,7 @@ function isSensitiveKey(key: string): boolean {
|
|
|
500
462
|
return /authorization|token|api[-_]?key/i.test(key);
|
|
501
463
|
}
|
|
502
464
|
|
|
503
|
-
async function prepareFixturePayload(
|
|
465
|
+
export async function prepareFixturePayload(
|
|
504
466
|
fixturePath: string,
|
|
505
467
|
payload: unknown,
|
|
506
468
|
append: boolean,
|
|
@@ -509,15 +471,34 @@ async function prepareFixturePayload(
|
|
|
509
471
|
return payload;
|
|
510
472
|
}
|
|
511
473
|
|
|
474
|
+
let fixtureSource: string;
|
|
512
475
|
try {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
476
|
+
fixtureSource = readFileSync(fixturePath, "utf8");
|
|
477
|
+
} catch (error) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`Cannot append to existing fixture "${fixturePath}" because it could not be read: ${
|
|
480
|
+
error instanceof Error ? error.message : String(error)
|
|
481
|
+
}. Fix its permissions or delete it, then run apifuse record --append again.`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let existing: unknown;
|
|
486
|
+
try {
|
|
487
|
+
existing = JSON.parse(fixtureSource) as unknown;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
`Cannot append to corrupt fixture "${fixturePath}" because it is not valid JSON: ${
|
|
491
|
+
error instanceof Error ? error.message : String(error)
|
|
492
|
+
}. Fix or delete the fixture, then run apifuse record --append again.`,
|
|
493
|
+
);
|
|
519
494
|
}
|
|
520
495
|
|
|
496
|
+
if (Array.isArray(existing)) {
|
|
497
|
+
return [...existing, payload];
|
|
498
|
+
}
|
|
499
|
+
if (existing !== null) {
|
|
500
|
+
return [existing, payload];
|
|
501
|
+
}
|
|
521
502
|
return payload;
|
|
522
503
|
}
|
|
523
504
|
|
|
@@ -529,4 +510,6 @@ function formatBytes(bytes: number): string {
|
|
|
529
510
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
530
511
|
}
|
|
531
512
|
|
|
532
|
-
|
|
513
|
+
if (import.meta.main) {
|
|
514
|
+
await main();
|
|
515
|
+
}
|