[API Docs] Add @track-adoption (#138366)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: spalger <spencer@elastic.co>
This commit is contained in:
Alejandro Fernández Haro 2022-09-07 17:39:31 +02:00 committed by GitHub
parent 2115309d0a
commit 27d93fdf69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 655 additions and 308 deletions

View file

@ -169,6 +169,8 @@ export interface IAnalyticsClient {
* Reports a telemetry event.
* @param eventType The event type registered via the `registerEventType` API.
* @param eventData The properties matching the schema declared in the `registerEventType` API.
*
* @track-adoption
*/
reportEvent: <EventTypeData extends object>(
eventType: EventType,
@ -198,8 +200,10 @@ export interface IAnalyticsClient {
*/
optIn: (optInConfig: OptInConfig) => void;
/**
* Registers the context provider to enrich the any reported events.
* Registers the context provider to enrich any reported events.
* @param contextProviderOpts {@link ContextProviderOpts}
*
* @track-adoption
*/
registerContextProvider: <Context>(contextProviderOpts: ContextProviderOpts<Context>) => void;
/**

View file

@ -50,7 +50,7 @@ export interface CiStatsMetric {
/** optional limit which will generate an error on PRs when the metric exceeds the limit */
limit?: number;
/**
* path, relative to the repo, where the config file contianing limits
* path, relative to the repo, where the config file containing limits
* is kept. Linked from PR comments instructing contributors how to fix
* their PRs.
*/

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test/jest_integration_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-docs-utils'],
};

View file

@ -11,7 +11,7 @@ import { Project, Node } from 'ts-morph';
import { ToolingLog } from '@kbn/tooling-log';
import { TypeKind, ApiScope, PluginOrPackage } from '../types';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';
import { getKibanaPlatformPlugin } from '../integration_tests/kibana_platform_plugin_mock';
import { getDeclarationNodesForPluginScope } from '../get_declaration_nodes_for_plugin';
import { buildApiDeclarationTopNode } from './build_api_declaration';
import { isNamedNode } from '../tsmorph_utils';
@ -29,7 +29,10 @@ function getNodeName(node: Node): string {
}
beforeAll(() => {
const tsConfigFilePath = Path.resolve(__dirname, '../tests/__fixtures__/src/tsconfig.json');
const tsConfigFilePath = Path.resolve(
__dirname,
'../integration_tests/__fixtures__/src/tsconfig.json'
);
const project = new Project({
tsConfigFilePath,
});

View file

@ -23,6 +23,7 @@ export function buildBasicApiDeclaration(node: Node, opts: BuildApiDecOpts): Api
const tags = getJSDocTags(node);
const deprecated = tags.find((t) => t.getTagName() === 'deprecated') !== undefined;
const removeByTag = tags.find((t) => t.getTagName() === 'removeBy');
const trackAdoption = tags.find((t) => t.getTagName() === 'track-adoption') !== undefined;
let label = opts.name;
@ -49,6 +50,7 @@ export function buildBasicApiDeclaration(node: Node, opts: BuildApiDecOpts): Api
path: getSourceForNode(node),
deprecated,
removeBy: removeByTag ? removeByTag.getCommentText() : undefined,
trackAdoption,
};
return {
...apiDec,

View file

@ -14,7 +14,7 @@ import { ApiScope, PluginOrPackage, Reference } from '../types';
import {
getKibanaPlatformPackage,
getKibanaPlatformPlugin,
} from '../tests/kibana_platform_plugin_mock';
} from '../integration_tests/kibana_platform_plugin_mock';
const plugin = getKibanaPlatformPlugin('pluginA');
const packageA = getKibanaPlatformPackage('@kbn/package-a');
@ -135,7 +135,7 @@ it('test full file imports with a matching plugin', () => {
"pluginId": "pluginA",
"scope": "public",
"section": undefined,
"text": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index",
"text": "packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_a/public/foo/index",
},
" something",
]

View file

@ -73,9 +73,8 @@ export function maybeCollectReferences({
apiDec,
captureReferences,
}: MaybeCollectReferencesOpt): ApiReference[] | undefined {
if (Node.isReferenceFindable(node)) {
return captureReferences || apiDec.deprecated
? getReferences({ node, plugins, currentPluginId, log })
: undefined;
const shouldCaptureReferences = captureReferences || apiDec.deprecated || apiDec.trackAdoption;
if (shouldCaptureReferences && Node.isReferenceFindable(node)) {
return getReferences({ node, plugins, currentPluginId, log });
}
}

View file

@ -25,7 +25,7 @@ import { writeDeprecationDocByApi } from './mdx/write_deprecations_doc_by_api';
import { writeDeprecationDocByPlugin } from './mdx/write_deprecations_doc_by_plugin';
import { writePluginDirectoryDoc } from './mdx/write_plugin_directory_doc';
import { collectApiStatsForPlugin } from './stats';
import { countEslintDisableLine, EslintDisableCounts } from './count_eslint_disable';
import { countEslintDisableLines, EslintDisableCounts } from './count_eslint_disable';
import { writeDeprecationDueByTeam } from './mdx/write_deprecations_due_by_team';
import { trimDeletedDocsFromNav } from './trim_deleted_docs_from_nav';
import { getAllDocFileIds } from './mdx/get_all_doc_file_ids';
@ -37,6 +37,7 @@ function isStringArray(arr: unknown | string[]): arr is string[] {
export function runBuildApiDocsCli() {
run(
async ({ log, flags }) => {
const collectReferences = flags.references as boolean;
const stats = flags.stats && typeof flags.stats === 'string' ? [flags.stats] : flags.stats;
const pluginFilter =
flags.plugin && typeof flags.plugin === 'string' ? [flags.plugin] : flags.plugin;
@ -77,13 +78,16 @@ export function runBuildApiDocsCli() {
await Fsp.mkdir(outputFolder, { recursive: true });
}
const collectReferences = flags.references as boolean;
const { pluginApiMap, missingApiItems, unreferencedDeprecations, referencedDeprecations } =
getPluginApiMap(project, plugins, log, {
collectReferences,
pluginFilter: pluginFilter as string[],
});
const {
pluginApiMap,
missingApiItems,
unreferencedDeprecations,
referencedDeprecations,
adoptionTrackedAPIs,
} = getPluginApiMap(project, plugins, log, {
collectReferences,
pluginFilter: pluginFilter as string[],
});
const reporter = CiStatsReporter.fromEnv(log);
@ -93,17 +97,22 @@ export function runBuildApiDocsCli() {
const pluginApi = pluginApiMap[id];
allPluginStats[id] = {
...(await countEslintDisableLine(plugin.directory)),
...collectApiStatsForPlugin(pluginApi, missingApiItems, referencedDeprecations),
...(await countEslintDisableLines(plugin.directory)),
...collectApiStatsForPlugin(
pluginApi,
missingApiItems,
referencedDeprecations,
adoptionTrackedAPIs
),
owner: plugin.manifest.owner,
description: plugin.manifest.description,
isPlugin: plugin.isPlugin,
};
}
writePluginDirectoryDoc(outputFolder, pluginApiMap, allPluginStats, log);
await writePluginDirectoryDoc(outputFolder, pluginApiMap, allPluginStats, log);
plugins.forEach((plugin) => {
for (const plugin of plugins) {
// Note that the filtering is done here, and not above because the entire public plugin API has to
// be parsed in order to correctly determine reference links, and ensure that `removeBrokenLinks`
// doesn't remove more links than necessary.
@ -153,6 +162,29 @@ export function runBuildApiDocsCli() {
group: 'References to deprecated APIs',
value: pluginStats.deprecatedAPIsReferencedCount,
},
{
id,
meta: {
pluginTeam,
// `meta` only allows primitives or string[]
// Also, each string is allowed to have a max length of 2056,
// so it's safer to stringify each element in the array over sending the entire array as stringified.
// My internal tests with 4 plugins using the same API gets to a length of 156 chars,
// so we should have enough room for tracking popular APIs.
// TODO: We can do a follow-up improvement to split the report if we find out we might hit the limit.
adoptionTrackedAPIs: pluginStats.adoptionTrackedAPIs.map((metric) =>
JSON.stringify(metric)
),
},
group: 'Adoption-tracked APIs',
value: pluginStats.adoptionTrackedAPIsCount,
},
{
id,
meta: { pluginTeam },
group: 'Adoption-tracked APIs that are not used anywhere',
value: pluginStats.adoptionTrackedAPIsUnreferencedCount,
},
{
id,
meta: { pluginTeam },
@ -174,7 +206,7 @@ export function runBuildApiDocsCli() {
]);
const getLink = (d: ApiDeclaration) =>
`https://github.com/elastic/kibana/tree/master/${d.path}#:~:text=${encodeURIComponent(
`https://github.com/elastic/kibana/tree/main/${d.path}#:~:text=${encodeURIComponent(
d.label
)}`;
@ -251,19 +283,19 @@ export function runBuildApiDocsCli() {
if (pluginStats.apiCount > 0) {
log.info(`Writing public API doc for plugin ${pluginApi.id}.`);
writePluginDocs(outputFolder, { doc: pluginApi, plugin, pluginStats, log });
await writePluginDocs(outputFolder, { doc: pluginApi, plugin, pluginStats, log });
} else {
log.info(`Plugin ${pluginApi.id} has no public API.`);
}
writeDeprecationDocByPlugin(outputFolder, referencedDeprecations, log);
writeDeprecationDueByTeam(outputFolder, referencedDeprecations, plugins, log);
writeDeprecationDocByApi(
await writeDeprecationDocByPlugin(outputFolder, referencedDeprecations, log);
await writeDeprecationDueByTeam(outputFolder, referencedDeprecations, plugins, log);
await writeDeprecationDocByApi(
outputFolder,
referencedDeprecations,
unreferencedDeprecations,
log
);
});
}
if (Object.values(pathsOutsideScopes).length > 0) {
log.warning(`Found paths outside of normal scope folders:`);

View file

@ -6,20 +6,32 @@
* Side Public License, v 1.
*/
import { countEslintDisableLine } from './count_eslint_disable';
import { countEslintDisableLines } from './count_eslint_disable';
/* eslint-disable no-console */
it('countEsLintDisableLine', async () => {
console.log('This is a test');
describe('countEslintDisableLines', () => {
test('number of "eslint-disable*" in a file', async () => {
console.log('This is a test');
// eslint-disable-next-line prefer-const
let test: string = '';
// eslint-disable-next-line prefer-const
let testVar: string = '';
const counts = await countEslintDisableLine(__filename);
expect(counts.eslintDisableLineCount).toBe(1);
expect(counts.eslintDisableFileCount).toBe(1);
const counts = await countEslintDisableLines(__filename);
expect(counts.eslintDisableLineCount).toBe(1);
expect(counts.eslintDisableFileCount).toBe(1);
// To avoid unused warning.
return test;
// To avoid unused warning.
return testVar;
});
test('number of "eslint-disable*" in a directory', async () => {
const counts = await countEslintDisableLines(__dirname);
expect(counts).toMatchInlineSnapshot(`
Object {
"eslintDisableFileCount": 3,
"eslintDisableLineCount": 8,
}
`);
});
});

View file

@ -6,29 +6,65 @@
* Side Public License, v 1.
*/
import { execSync } from 'child_process';
import { asyncMapWithLimit } from '@kbn/std';
import Fs from 'fs';
import Path from 'path';
export interface EslintDisableCounts {
eslintDisableLineCount: number;
eslintDisableFileCount: number;
}
export async function countEslintDisableLine(path: string): Promise<EslintDisableCounts> {
const disableCountOutputs = await Promise.all([
execSync(`grep -rE 'eslint-disable-next-line|eslint-disable-line' ${path} | wc -l`),
execSync(`grep -rE 'eslint-disable ' ${path} | wc -l`),
]);
const eslintDisableLineCount = Number.parseInt(disableCountOutputs[0].toString(), 10);
if (eslintDisableLineCount === undefined || isNaN(eslintDisableLineCount)) {
throw new Error(`Parsing ${disableCountOutputs[0]} failed to product a valid number`);
async function fetchAllFilePaths(path: string): Promise<string[]> {
if ((await Fs.promises.stat(path)).isFile()) {
return [path];
}
const eslintDisableFileCount = Number.parseInt(disableCountOutputs[1].toString(), 10);
if (eslintDisableFileCount === undefined || isNaN(eslintDisableFileCount)) {
throw new Error(`Parsing ${disableCountOutputs[1]} failed to product a valid number`);
const filePaths: string[] = [];
const dirContent = await Fs.promises.readdir(path, { withFileTypes: true });
for (const item of dirContent) {
const itemPath = Path.resolve(path, item.name);
if (item.isDirectory()) {
filePaths.push(...(await fetchAllFilePaths(itemPath)));
} else if (item.isFile()) {
filePaths.push(itemPath);
}
}
return { eslintDisableFileCount, eslintDisableLineCount };
return filePaths;
}
function findOccurrences(fileContent: string, regexp: RegExp): number {
// using the flag 'g' returns an array of found occurrences.
const matchingResults = fileContent.toString().match(new RegExp(regexp, 'g')) || [];
return matchingResults.length;
}
async function countEsLintDisableInFile(path: string): Promise<EslintDisableCounts> {
const fileContent = await Fs.promises.readFile(path, { encoding: 'utf8' });
return {
eslintDisableLineCount:
findOccurrences(fileContent, /eslint-disable-next-line/) +
findOccurrences(fileContent, /eslint-disable-line/),
eslintDisableFileCount: findOccurrences(fileContent, /eslint-disable\s/),
};
}
export async function countEslintDisableLines(path: string): Promise<EslintDisableCounts> {
const filePaths = await fetchAllFilePaths(path);
const allEslintDisableCounts = await asyncMapWithLimit(filePaths, 100, (filePath) =>
countEsLintDisableInFile(filePath)
);
return allEslintDisableCounts.reduce(
(acc, fileEslintDisableCounts) => {
return {
eslintDisableFileCount:
acc.eslintDisableFileCount + fileEslintDisableCounts.eslintDisableFileCount,
eslintDisableLineCount:
acc.eslintDisableLineCount + fileEslintDisableCounts.eslintDisableLineCount,
};
},
{ eslintDisableFileCount: 0, eslintDisableLineCount: 0 }
);
}

View file

@ -9,7 +9,8 @@
import { ToolingLog } from '@kbn/tooling-log';
import { Project } from 'ts-morph';
import { getPluginApi } from './get_plugin_api';
import {
import type {
AdoptionTrackedAPIsByPlugin,
ApiDeclaration,
MissingApiItemMap,
PluginApi,
@ -18,6 +19,7 @@ import {
UnreferencedDeprecationsByPlugin,
} from './types';
import { removeBrokenLinks } from './utils';
import { AdoptionTrackedAPIStats } from './types';
export function getPluginApiMap(
project: Project,
@ -29,10 +31,11 @@ export function getPluginApiMap(
missingApiItems: MissingApiItemMap;
referencedDeprecations: ReferencedDeprecationsByPlugin;
unreferencedDeprecations: UnreferencedDeprecationsByPlugin;
adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin;
} {
log.debug('Building plugin API map, getting missing comments, and collecting deprecations...');
const pluginApiMap: { [key: string]: PluginApi } = {};
plugins.map((plugin) => {
plugins.forEach((plugin) => {
const captureReferences =
collectReferences && (!pluginFilter || pluginFilter.indexOf(plugin.manifest.id) >= 0);
pluginApiMap[plugin.manifest.id] = getPluginApi(
@ -47,16 +50,52 @@ export function getPluginApiMap(
// Mapping of plugin id to the missing source API id to all the plugin API items that referenced this item.
const missingApiItems: { [key: string]: { [key: string]: string[] } } = {};
const referencedDeprecations: ReferencedDeprecationsByPlugin = {};
const unreferencedDeprecations: UnreferencedDeprecationsByPlugin = {};
const adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin = {};
plugins.forEach((plugin) => {
const id = plugin.manifest.id;
const pluginApi = pluginApiMap[id];
removeBrokenLinks(pluginApi, missingApiItems, pluginApiMap, log);
collectDeprecations(pluginApi, referencedDeprecations, unreferencedDeprecations);
collectAdoptionTrackedAPIs(pluginApi, adoptionTrackedAPIs);
});
return { pluginApiMap, missingApiItems, referencedDeprecations, unreferencedDeprecations };
return {
pluginApiMap,
missingApiItems,
referencedDeprecations,
unreferencedDeprecations,
adoptionTrackedAPIs,
};
}
function collectAdoptionTrackedAPIs(
pluginApi: PluginApi,
adoptionTrackedAPIsByPlugin: AdoptionTrackedAPIsByPlugin
) {
adoptionTrackedAPIsByPlugin[pluginApi.id] = [];
(['client', 'common', 'server'] as Array<'client' | 'server' | 'common'>).forEach((scope) => {
pluginApi[scope].forEach((api) => {
collectAdoptionForApi(api, adoptionTrackedAPIsByPlugin[pluginApi.id]);
});
});
}
function collectAdoptionForApi(
api: ApiDeclaration,
adoptionTrackedAPIs: AdoptionTrackedAPIStats[]
) {
const { id, label, tags = [], children, references = [] } = api;
if (tags.find((tag) => tag === 'track-adoption')) {
const uniqueReferences = new Set<string>(references.map(({ plugin }) => plugin));
adoptionTrackedAPIs.push({
trackedApi: { id, label },
references: [...uniqueReferences.values()],
});
}
if (children) {
children.forEach((child) => collectAdoptionForApi(child, adoptionTrackedAPIs));
}
}
function collectDeprecations(

View file

@ -93,7 +93,7 @@ function fnIsCorrect(fn: ApiDeclaration | undefined) {
expect(p5?.description?.length).toBe(1);
}
beforeAll(() => {
beforeAll(async () => {
const tsConfigFilePath = Path.resolve(__dirname, '__fixtures__/src/tsconfig.json');
const project = new Project({
tsConfigFilePath,
@ -109,30 +109,34 @@ beforeAll(() => {
pluginA.manifest.serviceFolders = ['foo'];
const plugins: PluginOrPackage[] = [pluginA, pluginB];
const { pluginApiMap, missingApiItems, referencedDeprecations } = getPluginApiMap(
project,
plugins,
log,
{ collectReferences: false }
);
const { pluginApiMap, missingApiItems, referencedDeprecations, adoptionTrackedAPIs } =
getPluginApiMap(project, plugins, log, { collectReferences: false });
doc = pluginApiMap.pluginA;
pluginAStats = collectApiStatsForPlugin(doc, missingApiItems, referencedDeprecations);
pluginAStats = collectApiStatsForPlugin(
doc,
missingApiItems,
referencedDeprecations,
adoptionTrackedAPIs
);
pluginBStats = collectApiStatsForPlugin(
pluginApiMap.pluginB,
missingApiItems,
referencedDeprecations
referencedDeprecations,
adoptionTrackedAPIs
);
mdxOutputFolder = Path.resolve(__dirname, 'snapshots');
writePluginDocs(mdxOutputFolder, { doc, plugin: pluginA, pluginStats: pluginAStats, log });
writePluginDocs(mdxOutputFolder, {
doc: pluginApiMap.pluginB,
plugin: pluginB,
pluginStats: pluginBStats,
log,
});
await Promise.all([
writePluginDocs(mdxOutputFolder, { doc, plugin: pluginA, pluginStats: pluginAStats, log }),
writePluginDocs(mdxOutputFolder, {
doc: pluginApiMap.pluginB,
plugin: pluginB,
pluginStats: pluginBStats,
log,
}),
]);
});
it('Stats', () => {

View file

@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginA
title: "pluginA"
image: https://source.unsplash.com/400x175/?github
description: API docs for the pluginA plugin
date: 2022-08-08
date: 2022-09-05
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA']
---
import pluginAObj from './plugin_a.devdocs.json';

View file

@ -13,8 +13,9 @@
"signature": [
"() => void"
],
"path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts",
"path": "packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts",
"deprecated": false,
"trackAdoption": false,
"children": [],
"returnComment": [],
"initialIsOpen": false
@ -33,8 +34,9 @@
"signature": [
"() => \"foo\""
],
"path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts",
"path": "packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_a/public/foo/index.ts",
"deprecated": false,
"trackAdoption": false,
"returnComment": [],
"children": [],
"initialIsOpen": false
@ -66,8 +68,9 @@
"signature": [
"\"COMMON VAR!\""
],
"path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts",
"path": "packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_a/common/foo/index.ts",
"deprecated": false,
"trackAdoption": false,
"initialIsOpen": false
}
],

View file

@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginA-foo
title: "pluginA.foo"
image: https://source.unsplash.com/400x175/?github
description: API docs for the pluginA.foo plugin
date: 2022-08-08
date: 2022-09-05
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA.foo']
---
import pluginAFooObj from './plugin_a_foo.devdocs.json';

View file

@ -29,8 +29,9 @@
},
"<any>"
],
"path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_b/public/index.ts",
"path": "packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_b/public/index.ts",
"deprecated": false,
"trackAdoption": false,
"children": [
{
"parentPluginId": "pluginB",
@ -49,8 +50,9 @@
},
"<any>"
],
"path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_b/public/index.ts",
"path": "packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_b/public/index.ts",
"deprecated": false,
"trackAdoption": false,
"isRequired": true
}
],

View file

@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/pluginB
title: "pluginB"
image: https://source.unsplash.com/400x175/?github
description: API docs for the pluginB plugin
date: 2022-08-08
date: 2022-09-05
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginB']
---
import pluginBObj from './plugin_b.devdocs.json';

View file

@ -31,7 +31,7 @@ export function buildPluginDeprecationsTable(
return `
## ${key}
| Deprecated API | Reference location(s) | Remove By |
| ---------------|-----------|-----------|
${Object.keys(groupedDeprecationReferences)
@ -50,7 +50,7 @@ export function buildPluginDeprecationsTable(
(ref) =>
`[${ref.path.substr(
ref.path.lastIndexOf(Path.sep) + 1
)}](https://github.com/elastic/kibana/tree/master/${
)}](https://github.com/elastic/kibana/tree/main/${
ref.path
}#:~:text=${encodeURIComponent(api.label)})`
)

View file

@ -11,7 +11,7 @@ import { Project } from 'ts-morph';
import { ToolingLog } from '@kbn/tooling-log';
import { PluginApi, PluginOrPackage } from '../types';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';
import { getKibanaPlatformPlugin } from '../integration_tests/kibana_platform_plugin_mock';
import { getPluginApi } from '../get_plugin_api';
import { splitApisByFolder } from './write_plugin_split_by_folder';
@ -23,7 +23,10 @@ const log = new ToolingLog({
let doc: PluginApi;
beforeAll(() => {
const tsConfigFilePath = Path.resolve(__dirname, '../tests/__fixtures__/src/tsconfig.json');
const tsConfigFilePath = Path.resolve(
__dirname,
'../integration_tests/__fixtures__/src/tsconfig.json'
);
const project = new Project({
tsConfigFilePath,
});

View file

@ -9,7 +9,7 @@
import moment from 'moment';
import { ToolingLog } from '@kbn/tooling-log';
import dedent from 'dedent';
import fs from 'fs';
import Fsp from 'fs/promises';
import Path from 'path';
import {
ApiReference,
@ -20,12 +20,12 @@ import {
import { AUTO_GENERATED_WARNING } from '../auto_generated_warning';
import { getPluginApiDocId } from '../utils';
export function writeDeprecationDocByApi(
export async function writeDeprecationDocByApi(
folder: string,
deprecationsByPlugin: ReferencedDeprecationsByPlugin,
unReferencedDeprecations: UnreferencedDeprecationsByPlugin,
log: ToolingLog
): void {
): Promise<void> {
const deprecationReferencesByApi = Object.values(deprecationsByPlugin).reduce(
(acc, deprecations) => {
deprecations.forEach((deprecation) => {
@ -111,5 +111,5 @@ ${Object.values(unReferencedDeprecations)
`);
fs.writeFileSync(Path.resolve(folder, 'deprecations_by_api.mdx'), mdx);
await Fsp.writeFile(Path.resolve(folder, 'deprecations_by_api.mdx'), mdx);
}

View file

@ -9,17 +9,17 @@
import moment from 'moment';
import { ToolingLog } from '@kbn/tooling-log';
import dedent from 'dedent';
import fs from 'fs';
import Fsp from 'fs/promises';
import Path from 'path';
import { ApiDeclaration, ApiReference, ReferencedDeprecationsByPlugin } from '../types';
import { AUTO_GENERATED_WARNING } from '../auto_generated_warning';
import { getPluginApiDocId } from '../utils';
export function writeDeprecationDocByPlugin(
export async function writeDeprecationDocByPlugin(
folder: string,
deprecationsByPlugin: ReferencedDeprecationsByPlugin,
log: ToolingLog
): void {
): Promise<void> {
const tableMdx = Object.keys(deprecationsByPlugin)
.sort()
.map((key) => {
@ -54,7 +54,7 @@ export function writeDeprecationDocByPlugin(
(ref) =>
`[${ref.path.substr(
ref.path.lastIndexOf(Path.sep) + 1
)}](https://github.com/elastic/kibana/tree/master/${
)}](https://github.com/elastic/kibana/tree/main/${
ref.path
}#:~:text=${encodeURIComponent(api.label)})`
)
@ -84,5 +84,5 @@ ${tableMdx}
`);
fs.writeFileSync(Path.resolve(folder, 'deprecations_by_plugin.mdx'), mdx);
await Fsp.writeFile(Path.resolve(folder, 'deprecations_by_plugin.mdx'), mdx);
}

View file

@ -8,7 +8,7 @@
import moment from 'moment';
import { ToolingLog } from '@kbn/tooling-log';
import dedent from 'dedent';
import fs from 'fs';
import Fsp from 'fs/promises';
import Path from 'path';
import {
ApiDeclaration,
@ -19,12 +19,12 @@ import {
import { AUTO_GENERATED_WARNING } from '../auto_generated_warning';
import { getPluginApiDocId } from '../utils';
export function writeDeprecationDueByTeam(
export async function writeDeprecationDueByTeam(
folder: string,
deprecationsByPlugin: ReferencedDeprecationsByPlugin,
plugins: PluginOrPackage[],
log: ToolingLog
): void {
): Promise<void> {
const groupedByTeam: ReferencedDeprecationsByPlugin = Object.keys(deprecationsByPlugin).reduce(
(teamMap: ReferencedDeprecationsByPlugin, pluginId: string) => {
const dueDeprecations = deprecationsByPlugin[pluginId].filter(
@ -80,7 +80,7 @@ export function writeDeprecationDueByTeam(
(ref) =>
`[${ref.path.substr(
ref.path.lastIndexOf(Path.sep) + 1
)}](https://github.com/elastic/kibana/tree/master/${
)}](https://github.com/elastic/kibana/tree/main/${
ref.path
}#:~:text=${encodeURIComponent(api.label)})`
)
@ -111,5 +111,5 @@ ${tableMdx}
`);
fs.writeFileSync(Path.resolve(folder, 'deprecations_by_team.mdx'), mdx);
await Fsp.writeFile(Path.resolve(folder, 'deprecations_by_team.mdx'), mdx);
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import moment from 'moment';
import fs from 'fs';
import Fsp from 'fs/promises';
import Path from 'path';
import dedent from 'dedent';
import { ToolingLog } from '@kbn/tooling-log';
@ -31,12 +31,12 @@ interface TotalStats {
/**
* @param folder The location the mdx file will be written too.
*/
export function writePluginDirectoryDoc(
export async function writePluginDirectoryDoc(
folder: string,
pluginApiMap: { [key: string]: PluginApi },
pluginStatsMap: { [key: string]: PluginMetaInfo },
log: ToolingLog
): void {
): Promise<void> {
log.debug(`Writing plugin directory file`);
const uniqueTeams: string[] = [];
@ -112,7 +112,7 @@ ${getDirectoryTable(pluginApiMap, pluginStatsMap, false)}
`) + '\n\n';
fs.writeFileSync(Path.resolve(folder, 'plugin_directory.mdx'), mdx);
await Fsp.writeFile(Path.resolve(folder, 'plugin_directory.mdx'), mdx);
}
function getDirectoryTable(

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import moment from 'moment';
import fs from 'fs';
import Fsp from 'fs/promises';
import Path from 'path';
import dedent from 'dedent';
import { PluginApi, ScopeApi } from '../types';
@ -30,15 +30,15 @@ import { WritePluginDocsOpts } from './types';
* @param doc Contains the information of the plugin that will be written into mdx.
* @param log Used for logging debug and error information.
*/
export function writePluginDocs(
export async function writePluginDocs(
folder: string,
{ doc, plugin, pluginStats, log }: WritePluginDocsOpts
): void {
): Promise<void> {
if (doc.serviceFolders) {
log.debug(`Splitting plugin ${doc.id}`);
writePluginDocSplitByFolder(folder, { doc, log, plugin, pluginStats });
await writePluginDocSplitByFolder(folder, { doc, log, plugin, pluginStats });
} else {
writePluginDoc(folder, { doc, plugin, pluginStats, log });
await writePluginDoc(folder, { doc, plugin, pluginStats, log });
}
}
@ -55,10 +55,10 @@ function hasPublicApi(doc: PluginApi): boolean {
* @param doc Contains the information of the plugin that will be written into mdx.
* @param log Used for logging debug and error information.
*/
export function writePluginDoc(
export async function writePluginDoc(
folder: string,
{ doc, log, plugin, pluginStats }: WritePluginDocsOpts
): void {
): Promise<void> {
if (!hasPublicApi(doc)) {
log.debug(`${doc.id} does not have a public api. Skipping.`);
return;
@ -113,7 +113,7 @@ ${
common: groupPluginApi(doc.common),
server: groupPluginApi(doc.server),
};
fs.writeFileSync(
await Fsp.writeFile(
Path.resolve(folder, fileName + '.devdocs.json'),
JSON.stringify(scopedDoc, null, 2)
);
@ -122,7 +122,7 @@ ${
mdx += scopApiToMdx(scopedDoc.server, 'Server', json, 'server');
mdx += scopApiToMdx(scopedDoc.common, 'Common', json, 'common');
fs.writeFileSync(Path.resolve(folder, fileName + '.mdx'), mdx);
await Fsp.writeFile(Path.resolve(folder, fileName + '.mdx'), mdx);
}
function getJsonName(name: string): string {

View file

@ -10,7 +10,7 @@ import { Project } from 'ts-morph';
import { ToolingLog } from '@kbn/tooling-log';
import { splitApisByFolder } from './write_plugin_split_by_folder';
import { getPluginApi } from '../get_plugin_api';
import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock';
import { getKibanaPlatformPlugin } from '../integration_tests/kibana_platform_plugin_mock';
import { PluginOrPackage } from '../types';
const log = new ToolingLog({

View file

@ -6,22 +6,28 @@
* Side Public License, v 1.
*/
import { asyncForEachWithLimit } from '@kbn/std';
import { snakeToCamel } from '../utils';
import { PluginApi, ApiDeclaration } from '../types';
import { writePluginDoc } from './write_plugin_mdx_docs';
import { WritePluginDocsOpts } from './types';
export function writePluginDocSplitByFolder(
// There is no science behind this 10.
// When it was first introduced, it was using synchronous APIs, so the concurrency was 1.
// Feel free to adapt it when more data is gathered.
const CONCURRENT_WRITES = 10;
export async function writePluginDocSplitByFolder(
folder: string,
{ doc, plugin, pluginStats, log }: WritePluginDocsOpts
) {
const apisByFolder = splitApisByFolder(doc);
log.debug(`Split ${doc.id} into ${apisByFolder.length} services`);
apisByFolder.forEach((docDef) => {
await asyncForEachWithLimit(apisByFolder, CONCURRENT_WRITES, async (docDef) => {
// TODO: we should probably see if we can break down these stats by service folder. As it is, they will represent stats for
// the entire plugin.
writePluginDoc(folder, { doc: docDef, plugin, pluginStats, log });
await writePluginDoc(folder, { doc: docDef, plugin, pluginStats, log });
});
}

View file

@ -7,18 +7,20 @@
*/
import {
ApiDeclaration,
ApiStats,
MissingApiItemMap,
PluginApi,
ReferencedDeprecationsByPlugin,
type AdoptionTrackedAPIsByPlugin,
type ApiDeclaration,
type ApiStats,
type MissingApiItemMap,
type PluginApi,
type ReferencedDeprecationsByPlugin,
TypeKind,
} from './types';
export function collectApiStatsForPlugin(
doc: PluginApi,
missingApiItems: MissingApiItemMap,
deprecations: ReferencedDeprecationsByPlugin
deprecations: ReferencedDeprecationsByPlugin,
adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin
): ApiStats {
const stats: ApiStats = {
missingComments: [],
@ -26,6 +28,9 @@ export function collectApiStatsForPlugin(
noReferences: [],
deprecatedAPIsReferencedCount: 0,
unreferencedDeprecatedApisCount: 0,
adoptionTrackedAPIs: [],
adoptionTrackedAPIsCount: 0,
adoptionTrackedAPIsUnreferencedCount: 0,
apiCount: countApiForPlugin(doc),
missingExports: Object.values(missingApiItems[doc.id] ?? {}).length,
};
@ -39,9 +44,24 @@ export function collectApiStatsForPlugin(
collectStatsForApi(def, stats, doc);
});
stats.deprecatedAPIsReferencedCount = deprecations[doc.id] ? deprecations[doc.id].length : 0;
collectAdoptionTrackedAPIStats(doc, stats, adoptionTrackedAPIs);
return stats;
}
function collectAdoptionTrackedAPIStats(
doc: PluginApi,
stats: ApiStats,
adoptionTrackedAPIs: AdoptionTrackedAPIsByPlugin
) {
stats.adoptionTrackedAPIs = adoptionTrackedAPIs[doc.id] || [];
stats.adoptionTrackedAPIsCount = stats.adoptionTrackedAPIs.length;
stats.adoptionTrackedAPIsUnreferencedCount = stats.adoptionTrackedAPIs.filter(
({ references }) => references.length === 0
).length;
}
function collectStatsForApi(doc: ApiDeclaration, stats: ApiStats, pluginApi: PluginApi): void {
const missingComment = doc.description === undefined || doc.description.length === 0;
// Ignore all stats coming from third party libraries, we can't fix that!

View file

@ -198,6 +198,11 @@ export interface ApiDeclaration {
* Is this API deprecated or not?
*/
deprecated?: boolean;
/**
* Are we interested in tracking adoption of this API?
*/
trackAdoption?: boolean;
}
/**
@ -234,7 +239,7 @@ export interface ReferencedDeprecationsByPlugin {
[key: string]: Array<{ deprecatedApi: ApiDeclaration; ref: ApiReference }>;
}
// A mapping of plugin owner to it's plugin deprecation list.
// A mapping of plugin owner to its plugin deprecation list.
export interface ReferencedDeprecationsByTeam {
// Key is the plugin owner.
[key: string]: ReferencedDeprecationsByPlugin;
@ -245,6 +250,23 @@ export interface UnreferencedDeprecationsByPlugin {
[key: string]: ApiDeclaration[];
}
export interface AdoptionTrackedAPIStats {
/**
* Minimal identifiers for the tracked API.
*/
trackedApi: { id: string; label: string };
/**
* List of plugins where the API is used. For stats that is more than enough.
*/
references: string[];
}
// A mapping of plugin id to a list of every deprecated API it uses, and where it's referenced.
export interface AdoptionTrackedAPIsByPlugin {
// Key is the plugin id.
[key: string]: AdoptionTrackedAPIStats[];
}
// A mapping of deprecated API id to the places that are still referencing it.
export interface ReferencedDeprecationsByAPI {
[key: string]: { deprecatedApi: ApiDeclaration; references: ApiReference[] };
@ -258,6 +280,15 @@ export interface ApiStats {
missingExports: number;
deprecatedAPIsReferencedCount: number;
unreferencedDeprecatedApisCount: number;
adoptionTrackedAPIs: AdoptionTrackedAPIStats[];
/**
* Total number of APIs that the plugin wants to track the adoption for.
*/
adoptionTrackedAPIsCount: number;
/**
* Number of adoption-tracked APIs that are still not referenced.
*/
adoptionTrackedAPIsUnreferencedCount: number;
}
export type PluginMetaInfo = ApiStats & {

View file

@ -11,7 +11,7 @@ import Path from 'path';
import { Project } from 'ts-morph';
import { findPlugins } from './find_plugins';
import { getPluginApi } from './get_plugin_api';
import { getKibanaPlatformPlugin } from './tests/kibana_platform_plugin_mock';
import { getKibanaPlatformPlugin } from './integration_tests/kibana_platform_plugin_mock';
import { PluginApi, PluginOrPackage } from './types';
import { getPluginForPath, getServiceForPath, removeBrokenLinks, getFileName } from './utils';
@ -53,14 +53,17 @@ it('test getServiceForPath', () => {
expect(
getServiceForPath(
'/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index',
'/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a'
'/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_a/public/foo/index',
'/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/integration_tests/__fixtures__/src/plugin_a'
)
).toBe('foo');
});
it('test removeBrokenLinks', () => {
const tsConfigFilePath = Path.resolve(__dirname, 'tests/__fixtures__/src/tsconfig.json');
const tsConfigFilePath = Path.resolve(
__dirname,
'integration_tests/__fixtures__/src/tsconfig.json'
);
const project = new Project({
tsConfigFilePath,
});

View file

@ -81,6 +81,8 @@ export interface TelemetryPluginStart {
* Resolves `true` if the user has opted into send Elastic usage data.
* Resolves `false` if the user explicitly opted out of sending usage data to Elastic
* or did not choose to opt-in or out -yet- after a minor or major upgrade (only when previously opted-out).
*
* @track-adoption
*/
getIsOptedIn: () => Promise<boolean>;
}