feat: add vendor extensions metric with per-extension counts - #3021
feat: add vendor extensions metric with per-extension counts#3021n0rahh wants to merge 20 commits into
Conversation
🦋 Changeset detectedLatest commit: 06d4e23 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Performance Benchmark (Lower is Faster)
|
|
📦 A new experimental 🧪 version v0.0.0-snapshot.1785934483 of Redocly CLI has been published for testing. Install with NPM: npm install @redocly/cli@0.0.0-snapshot.1785934483 |
… to $ref and ignore map keys starting with x-
| const EMAIL_REGEX = /\S@\S+\.\S/; | ||
| const URL_SCHEME_REGEX = /:\/\//; | ||
|
|
||
| export const StatsSpecExtensions = (accumulator: SpecVendorExtensionsAccumulator) => { |
There was a problem hiding this comment.
@n0rahh
Why did you decide to create a separate rule to collect extensions instead of modifying the existing one?
There was a problem hiding this comment.
This duplicates the existing codebase and only adds one additional piece of functionality (collecting x-extensions).
Could you please consider extending the existing stats rule instead of introducing a separate one?
There was a problem hiding this comment.
@DmitryAnansky The existing stats rules are per-spec (StatsOAS, StatsAsync2, StatsAsync3), so putting extension collection there would mean duplicating it in all three — as a separate spec-agnostic visitor it's written once and just composed with whichever stats rule runs. But if you prefer to put it into existing rules - let me know, I can change it
There was a problem hiding this comment.
Stats command is spec oriented, as might content different data, but why would extensions be mixed in this case?
I guess you can reuse existing accumulator and reuse code and not duplicate for all 3 specs.
cc: @tatomyr
|
📦 A new experimental 🧪 version v0.0.0-snapshot.1786010740 of Redocly CLI has been published for testing. Install with NPM: npm install @redocly/cli@0.0.0-snapshot.1786010740 |
| } | ||
|
|
||
| export function applySpecExtensionsStats( | ||
| accumulator: SpecVendorExtensionsAccumulator, |
There was a problem hiding this comment.
| accumulator: SpecVendorExtensionsAccumulator, | |
| collectedExtensions: SpecVendorExtensionsAccumulator, |
?
| const extensions: SpecVendorExtensionsAccumulator = {}; | ||
|
|
||
| return { | ||
| any: { |
There was a problem hiding this comment.
Have you tried this?
| any: { | |
| SpecExtension: { |
This would be a single entrypoint.
If it doesn't help, I'd like to make it work first.
| counts?: Record<string, number>; | ||
| details?: SpecVendorExtensionsAccumulator; |
There was a problem hiding this comment.
Can we just put everything into details and use what we need? Why creating 2 fields for the same purpose. If you need separate fields, you can put them inside the details placeholder, like so:
details:
extensionCounts: ...
someExtraStats: ...
| ) { | ||
| if (!isPlainObject(node)) return; | ||
| for (const [key, value] of Object.entries(node)) { | ||
| if (!key.startsWith(EXTENSION_PREFIX)) continue; |
There was a problem hiding this comment.
This looks wrong since it will collect any x- properties defined in Schemas which are not extensions.
| function isMapEntryKey(type: NormalizedNodeType, key: string, value: unknown): boolean { | ||
| if (getOwn(type.properties, key) !== undefined) return false; | ||
| const entryType = | ||
| typeof type.additionalProperties === 'function' |
There was a problem hiding this comment.
This does the walker's job. Let's leverage the visitor pattern to get to the node you need.
| }; | ||
| } | ||
|
|
||
| async function collect(yaml: string): Promise<SpecVendorExtensionsAccumulator> { |
There was a problem hiding this comment.
This is too complicated as a test helper. If you cannot setup unit tests in a simpler manner, please use e2e tests instead.
|
I like this idea |
…unts and new test cases
…for vendor extensions
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 561ede2. Configure here.
… in OpenAPI specs
|
📦 A new experimental 🧪 version v0.0.0-snapshot.1786099785 of Redocly CLI has been published for testing. Install with NPM: npm install @redocly/cli@0.0.0-snapshot.1786099785 |
|
I found things I didn't know existed in there. Thank you. |

What/Why/How?
Adds a Vendor Extensions metric to the
statscommand. It reports how many distinctx-extensions a document uses and how many times each one occurs, shown in thestylish,json, andmarkdownoutput. Works across OpenAPI and AsyncAPI.The stats visitors collect extensions through a single
SpecExtensionentrypoint. Two walker fixes make that possible:SpecExtensionnodes now dedupe by location, so every occurrence is visited — previously occurrences with equal scalar values (e.g.x-internal: trueon many operations) were visited only once. This also means visitors and configurable rules targetingSpecExtensionnow fire per occurrence.x-properties with a declared type (e.g.x-codeSamples) were walked twice, which inflated other metrics.For the stats walk only,
ensureSpecExtensionDispatchadjusts the command's normalized types so everyx-key dispatches asSpecExtension— including natively-typed extensions and AsyncAPI types that don't declareextensionsPrefix. The structural extensionsx-webhooksandx-querykeep their declared types so the webhooks/operations/tags metrics still traverse their subtrees; the visitors count those two explicitly. Lint and bundle behavior is unchanged.The collector also gathers per-extension prop names and value samples for the portal's stats collector (telemetry) via the accumulator — the CLI prints only totals and counts. Samples are bounded (20 props / 20 values per extension), long strings become a
<string:N>marker, and credential-like keys and values are masked.Reference
Testing
Covered with unit and e2e tests.
Published snapshot and tested
clicommands in terminal.Screenshots (optional)
Check yourself
Security
Note
Medium Risk
Walker deduplication and extension-property traversal changes apply globally to lint and other walks, not only stats, which can alter rule firing for
SpecExtensionand slightly change metric accuracy elsewhere.Overview
Adds a Vendor Extensions (
🧩) line toredocly statsfor OpenAPI and AsyncAPI: the total is how many distinctx-names appear, and stylish / JSON / markdown outputs list each extension with an occurrence count.Stats collection routes almost every
x-key through aSpecExtensionvisitor viaensureSpecExtensionDispatch(used only on the stats walk).x-webhooksandx-querystay on their structural types so webhook/operation metrics still walk those trees; those two names are counted explicitly in the OAS stats visitor.finalizeStatsalso centralizes finishing Set-based metrics (refs, tags, etc.).The walker now dedupes
SpecExtensionvisits by document location instead of node value, so repeated scalars likex-internal: trueare counted separately, and extension keys that were both typed and listed underextensionsPrefixare no longer walked twice.Behind the CLI,
collectSpecExtensionrecords bounded value samples (for portal telemetry) with length caps, truncation, and masking of sensitive keys/values; printed output uses only totals and per-name counts.Reviewed by Cursor Bugbot for commit 06d4e23. Bugbot is set up for automated code reviews on this repo. Configure here.