Skip to content

feat: add vendor extensions metric with per-extension counts - #3021

Open
n0rahh wants to merge 20 commits into
mainfrom
feat/stats-for-vendor-extensions
Open

feat: add vendor extensions metric with per-extension counts#3021
n0rahh wants to merge 20 commits into
mainfrom
feat/stats-for-vendor-extensions

Conversation

@n0rahh

@n0rahh n0rahh commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What/Why/How?

Adds a Vendor Extensions metric to the stats command. It reports how many distinct x- extensions a document uses and how many times each one occurs, shown in the stylish, json, and markdown output. Works across OpenAPI and AsyncAPI.

The stats visitors collect extensions through a single SpecExtension entrypoint. Two walker fixes make that possible:

  • SpecExtension nodes now dedupe by location, so every occurrence is visited — previously occurrences with equal scalar values (e.g. x-internal: true on many operations) were visited only once. This also means visitors and configurable rules targeting SpecExtension now fire per occurrence.
  • Fixed a pre-existing bug where x- properties with a declared type (e.g. x-codeSamples) were walked twice, which inflated other metrics.

For the stats walk only, ensureSpecExtensionDispatch adjusts the command's normalized types so every x- key dispatches as SpecExtension — including natively-typed extensions and AsyncAPI types that don't declare extensionsPrefix. The structural extensions x-webhooks and x-query keep 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 cli commands in terminal.

Screenshots (optional)

image image image

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

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 SpecExtension and slightly change metric accuracy elsewhere.

Overview
Adds a Vendor Extensions (🧩) line to redocly stats for OpenAPI and AsyncAPI: the total is how many distinct x- names appear, and stylish / JSON / markdown outputs list each extension with an occurrence count.

Stats collection routes almost every x- key through a SpecExtension visitor via ensureSpecExtensionDispatch (used only on the stats walk). x-webhooks and x-query stay on their structural types so webhook/operation metrics still walk those trees; those two names are counted explicitly in the OAS stats visitor. finalizeStats also centralizes finishing Set-based metrics (refs, tags, etc.).

The walker now dedupes SpecExtension visits by document location instead of node value, so repeated scalars like x-internal: true are counted separately, and extension keys that were both typed and listed under extensionsPrefix are no longer walked twice.

Behind the CLI, collectSpecExtension records 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.

@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 06d4e23

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@redocly/openapi-core Minor
@redocly/cli Minor
@redocly/client-generator Patch
@redocly/respect-core Minor

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 77.22% (🎯 77%) 11707 / 15160
🔵 Statements 77.32% (🎯 77%) 12537 / 16214
🔵 Functions 81.16% (🎯 81%) 2361 / 2909
🔵 Branches 70.93% (🎯 70%) 8628 / 12164
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/stats/index.ts 0% 0% 0% 0% 28-71
packages/cli/src/commands/stats/visitor-and-accumulator-resolver.ts 0% 0% 0% 0% 13-57
packages/cli/src/commands/stats/print-stats/json.ts 0% 0% 0% 0% 8-19
packages/cli/src/commands/stats/print-stats/markdown.ts 0% 0% 0% 0% 10-25
packages/cli/src/commands/stats/print-stats/stylish.ts 0% 0% 0% 0% 11-17
packages/core/src/walk.ts 98.24% 97.51% 85% 98.75% 220, 235, 459
packages/core/src/rules/other/stats.ts 4.76% 0% 0% 4.76% 23-31, 36-119, 126-182, 189-247
packages/core/src/utils/spec-extensions.ts 75.92% 70.37% 75% 76.08% 29-41, 71, 81
Generated in workflow #11238 for commit 06d4e23 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
cli-latest ▓ 1.00x (Fastest) ▓ 1.01x ± 0.01 ▓ 1.00x (Fastest)
cli-next ▓ 1.01x ± 0.01 ▓ 1.00x (Fastest) ▓ 1.00x ± 0.01

@n0rahh n0rahh added the snapshot Create experimental release PR label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📦 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

⚠️ Note: This is a development build and may contain unstable features.

@n0rahh
n0rahh marked this pull request as ready for review August 5, 2026 13:45
@n0rahh
n0rahh requested review from a team as code owners August 5, 2026 13:45
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
… to $ref and ignore map keys starting with x-
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
Comment thread .changeset/seven-waves-create.md Outdated
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
const EMAIL_REGEX = /\S@\S+\.\S/;
const URL_SCHEME_REGEX = /:\/\//;

export const StatsSpecExtensions = (accumulator: SpecVendorExtensionsAccumulator) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@n0rahh
Why did you decide to create a separate rule to collect extensions instead of modifying the existing one?

@DmitryAnansky DmitryAnansky Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@n0rahh n0rahh added snapshot Create experimental release PR and removed snapshot Create experimental release PR labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📦 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

⚠️ Note: This is a development build and may contain unstable features.

Comment thread packages/core/src/utils/spec-extensions.ts Outdated
@n0rahh
n0rahh requested a review from DmitryAnansky August 6, 2026 10:07
Comment thread .changeset/seven-waves-create.md Outdated
Comment thread docs/@v2/commands/stats.md
Comment thread docs/@v2/commands/stats.md
Comment thread tests/e2e/stats/stats-async2-json/snapshot.txt
@n0rahh
n0rahh requested a review from DmitryAnansky August 6, 2026 14:14
}

export function applySpecExtensionsStats(
accumulator: SpecVendorExtensionsAccumulator,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
accumulator: SpecVendorExtensionsAccumulator,
collectedExtensions: SpecVendorExtensionsAccumulator,

?

Comment thread packages/core/src/rules/other/stats.ts Outdated
const extensions: SpecVendorExtensionsAccumulator = {};

return {
any: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried this?

Suggested change
any: {
SpecExtension: {

This would be a single entrypoint.
If it doesn't help, I'd like to make it work first.

Comment thread packages/core/src/typings/common.ts Outdated
Comment on lines +6 to +7
counts?: Record<string, number>;
details?: SpecVendorExtensionsAccumulator;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too complicated as a test helper. If you cannot setup unit tests in a simpler manner, please use e2e tests instead.

@adamaltman

Copy link
Copy Markdown
Member

I like this idea

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/core/src/rules/other/stats.ts Outdated
@n0rahh n0rahh added snapshot Create experimental release PR and removed snapshot Create experimental release PR labels Aug 7, 2026
@n0rahh
n0rahh requested a review from tatomyr August 7, 2026 10:49
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📦 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

⚠️ Note: This is a development build and may contain unstable features.

@adamaltman

Copy link
Copy Markdown
Member

I found things I didn't know existed in there. Thank you.

Document: ./website/api-definitions/openapi/openapi.yaml stats:

🚗 References: 972
📦 External Documents: 0
📈 Schemas: 838
👉 Parameters: 61
🔗 Links: 0
🔀 Path Items: 382
🎣 Webhooks: 148
👷 Operations: 613
🔖 Tags: 88
🧩 Vendor Extensions: 16
  - x-badges: 151
  - x-basic: 92
  - x-codeSamples: 261
  - x-currency-field: 16
  - x-enumDescriptions: 79
  - x-is-free-form: 1
  - x-label: 26
  - x-logo: 1
  - x-multiline: 31
  - x-products: 761
  - x-sdk-operation-name: 613
  - x-sdk-resource: 43
  - x-sdk-skip: 5
  - x-sortable: 155
  - x-tagGroups: 1
  - x-type: 138

./website/api-definitions/openapi/openapi.yaml: stats processed in 143ms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

snapshot Create experimental release PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants