Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESLint Telemetry Rule #151173

Closed
wants to merge 58 commits into from
Closed
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a25c970
Tryout
CoenWarmer Oct 14, 2022
7ab8847
First pass of eslint rule
CoenWarmer Oct 18, 2022
4f7ae81
Second pass of ESLint rule
CoenWarmer Oct 29, 2022
1ccdcd9
Merge branch 'main' of github.com:elastic/kibana into feature/telemetry
CoenWarmer Nov 10, 2022
250e804
Merge branch 'main' of github.com:elastic/kibana into feature/telemetry
CoenWarmer Dec 7, 2022
3f8f487
Fix Typescript typing, clean up function
CoenWarmer Dec 8, 2022
dedb910
[CI] Auto-commit changed files from 'node scripts/generate codeowners'
kibanamachine Dec 8, 2022
85d7367
Update Bazel file
CoenWarmer Dec 8, 2022
baa688a
Merge branch 'feature/telemetry' of github.com:CoenWarmer/kibana into…
CoenWarmer Dec 8, 2022
403d279
Remove test files
CoenWarmer Dec 8, 2022
5ac3bcb
Update docs
CoenWarmer Dec 8, 2022
5266a01
Cleanup
CoenWarmer Dec 8, 2022
14cf066
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 10, 2023
336f3e8
Tweak generated suggestion
CoenWarmer Feb 10, 2023
88fb78f
Add more typesafety
CoenWarmer Feb 14, 2023
ba0f575
Add tests
CoenWarmer Feb 14, 2023
d76c486
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 14, 2023
d1c9023
Update readme
CoenWarmer Feb 14, 2023
105e7db
Update package.json
CoenWarmer Feb 14, 2023
79ed102
[CI] Auto-commit changed files from 'node scripts/generate codeowners'
kibanamachine Feb 14, 2023
4223a83
Revert CODEOWNERS
CoenWarmer Feb 14, 2023
7f1e8aa
Merge branch 'feature/telemetry-renewed' of github.com:CoenWarmer/kib…
CoenWarmer Feb 14, 2023
c0e95f7
Revert CODEOWNERS
CoenWarmer Feb 14, 2023
e6ab2ef
Fix unnecessary main in plugin package.json
CoenWarmer Feb 14, 2023
401026c
Run code owners script locally
CoenWarmer Feb 15, 2023
ad65ba7
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 15, 2023
56354c1
[CI] Auto-commit changed files from 'node scripts/generate codeowners'
kibanamachine Feb 15, 2023
c60b073
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 15, 2023
290693f
Merge branch 'feature/telemetry-renewed' of github.com:CoenWarmer/kib…
CoenWarmer Feb 15, 2023
4211a7e
Support const function definitions and HOCs
CoenWarmer Feb 15, 2023
48316dd
Support const function definitions and HOCs
CoenWarmer Feb 15, 2023
3a99ee9
Merge branch 'feature/telemetry-renewed' of github.com:CoenWarmer/kib…
CoenWarmer Feb 15, 2023
b7f1bd0
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 15, 2023
9c20ae7
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Feb 15, 2023
edd2840
Readability tweaks
CoenWarmer Feb 15, 2023
03fd329
Merge branch 'feature/telemetry-renewed' of github.com:CoenWarmer/kib…
CoenWarmer Feb 15, 2023
eab80fb
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 15, 2023
056bd6d
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Feb 15, 2023
17f4627
Revert changes made by CI
CoenWarmer Feb 15, 2023
24efb17
Fix test
CoenWarmer Feb 15, 2023
bdc07c2
Remove extraneous Bazel files
CoenWarmer Feb 16, 2023
06461ce
Use dynamic repo folders in getAppName
CoenWarmer Feb 16, 2023
a8a0b01
Revert accidental code change
CoenWarmer Feb 16, 2023
77f169d
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 16, 2023
05d0b7f
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Feb 16, 2023
21e4d93
Fix test
CoenWarmer Feb 16, 2023
cb0a3ba
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 16, 2023
fe44171
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Feb 16, 2023
a9721e1
Revert CI made changes
CoenWarmer Feb 16, 2023
4081bbf
Set rule to error instead of warn
CoenWarmer Feb 17, 2023
42f0f6a
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 17, 2023
00ceda3
Add Should run in Editor check
CoenWarmer Feb 21, 2023
d42309e
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Feb 21, 2023
26cec8a
Revert earlier --fix check
CoenWarmer Feb 21, 2023
c08261e
Add running in editor check
CoenWarmer Feb 21, 2023
a8ca293
Merge branch 'main' of github.com:elastic/kibana into feature/telemet…
CoenWarmer Mar 6, 2023
5efa52b
Dont use running in editor check
CoenWarmer Mar 6, 2023
dd05f03
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Mar 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ packages/kbn-eslint-config @elastic/kibana-operations
packages/kbn-eslint-plugin-disable @elastic/kibana-operations
packages/kbn-eslint-plugin-eslint @elastic/kibana-operations
packages/kbn-eslint-plugin-imports @elastic/kibana-operations
packages/kbn-eslint-plugin-telemetry @elastic/actionable-observability
x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security
src/plugins/event_annotation @elastic/kibana-visualizations
x-pack/test/plugin_api_integration/plugins/event_log @elastic/response-ops
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,7 @@
"@kbn/eslint-plugin-disable": "link:packages/kbn-eslint-plugin-disable",
"@kbn/eslint-plugin-eslint": "link:packages/kbn-eslint-plugin-eslint",
"@kbn/eslint-plugin-imports": "link:packages/kbn-eslint-plugin-imports",
"@kbn/eslint-plugin-telemetry": "link:packages/kbn-eslint-plugin-telemetry",
"@kbn/expect": "link:packages/kbn-expect",
"@kbn/failed-test-reporter-cli": "link:packages/kbn-failed-test-reporter-cli",
"@kbn/find-used-node-modules": "link:packages/kbn-find-used-node-modules",
Expand Down
749 changes: 749 additions & 0 deletions packages/BUILD.bazel

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/kbn-eslint-config/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
'@kbn/eslint-plugin-disable',
'@kbn/eslint-plugin-eslint',
'@kbn/eslint-plugin-imports',
'@kbn/eslint-plugin-telemetry',
'prettier',
],

Expand Down Expand Up @@ -297,5 +298,6 @@ module.exports = {
'@kbn/imports/uniform_imports': 'error',
'@kbn/imports/no_unused_imports': 'error',
'@kbn/imports/no_boundary_crossing': 'error',
'@kbn/telemetry/event_generating_elements_should_be_instrumented': 'warn',
},
};
129 changes: 129 additions & 0 deletions packages/kbn-eslint-plugin-telemetry/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")

PKG_DIRNAME = "kbn-eslint-plugin-telemetry"
PKG_REQUIRE_NAME = "@kbn/eslint-plugin-telemetry"

SOURCE_FILES = glob(
[
"**/*.ts",
],
exclude = [
"**/*.config.js",
"**/*.mock.*",
"**/*.test.*",
"**/*.stories.*",
"**/__snapshots__/**",
"**/integration_tests/**",
"**/mocks/**",
"**/scripts/**",
"**/storybook/**",
"**/test_fixtures/**",
"**/test_helpers/**",
],
)

SRCS = SOURCE_FILES

filegroup(
name = "srcs",
srcs = SRCS,
)

NPM_MODULE_EXTRA_FILES = [
"package.json",
]

# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
# "@npm//@typescript-eslint/typescript-estree",
# "@npm//@types/estree",
# "@npm//eslint",
]

# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"@npm//@types/node",
"@npm//@types/jest",
"@npm//@types/eslint",
"@npm//@typescript-eslint/typescript-estree",
]

jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)

ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)

ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
declaration_map = True,
emit_declaration_only = True,
out_dir = "target_types",
tsconfig = ":tsconfig",
)

js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)

pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)

filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)

pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)

filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)
13 changes: 13 additions & 0 deletions packages/kbn-eslint-plugin-telemetry/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: kibDevDocsOpsEslintPluginTelemetry
slug: /kibana-dev-docs/ops/kbn-eslint-plugin-telemetry
title: '@kbn/eslint-plugin-telemetry'
description: Custom ESLint rules to support telemetry in the Kibana repository
tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'telemetry']
---

`@kbn/eslint-plugin-telemetry` is an ESLint plugin providing custom rules for validating JSXCode in the Kibana repo to make sure it can be instrumented for the purposes of telemetry.

## `@kbn/telemetry/instrumentable_elements_should_be_instrumented`

This rule warns engineers to add `data-test-subj` to instrumentable components. It currently suggests the most widely used EUI components (`EuiButton`, `EuiFieldText`, etc), but can be expanded with often-used specific components used in the Kibana repo.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.
*/

import { getAppName } from './get_app_name';

const SYSTEMPATH = 'systemPath';

const testMap = [
['x-pack/plugins/observability/foo/bar/baz/header_actions.tsx', 'o11y'],
['x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx', 'apm'],
['x-pack/plugins/cases/public/components/foo.tsx', 'cases'],
['packages/kbn-alerts-ui-shared/src/alert_lifecycle_status_badge/index.tsx', 'kbnAlertsUiShared'],
];

describe('Get App Name', () => {
test.each(testMap)(
'should get the responsible app name from a file path',
(path, expectedValue) => {
const appName = getAppName(`${SYSTEMPATH}/${path}`, SYSTEMPATH);
expect(appName).toBe(expectedValue);
}
);
});
26 changes: 26 additions & 0 deletions packages/kbn-eslint-plugin-telemetry/helpers/get_app_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.
*/

import { camelCase } from 'lodash';
import path from 'path';

const XPACK_DIR_ROOT = '/x-pack/plugins';
const PACKAGES_ROOT = '/packages';

export function getAppName(fileName: string, cwd: string) {
const { dir } = path.parse(fileName);
const relativePathToFile = dir.replace(cwd, '');

const appName = relativePathToFile.includes(XPACK_DIR_ROOT)
? relativePathToFile.split(XPACK_DIR_ROOT)[1].split('/')[1]
: relativePathToFile.includes(PACKAGES_ROOT)
? relativePathToFile.split(PACKAGES_ROOT)[1].split('/')[1]
: '';

return appName === 'observability' ? 'o11y' : camelCase(appName);
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.
*/

import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree';

export function getFunctionName(func: TSESTree.FunctionDeclaration | TSESTree.Node): string {
if (
'id' in func &&
func.id &&
func.type === AST_NODE_TYPES.FunctionDeclaration &&
func.id.type === AST_NODE_TYPES.Identifier
) {
return func.id.name;
}

if (
func.parent &&
(func.parent.type !== AST_NODE_TYPES.VariableDeclarator ||
func.parent.id.type !== AST_NODE_TYPES.Identifier)
) {
return getFunctionName(func.parent);
}

if (func.parent?.id && 'name' in func.parent.id) {
return func.parent.id.name;
}

return '';
}
133 changes: 133 additions & 0 deletions packages/kbn-eslint-plugin-telemetry/helpers/get_intent_from_node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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.
*/
import { TSESTree } from '@typescript-eslint/typescript-estree';
import camelCase from 'lodash/camelCase';

/*
Attempts to get a string representation of the intent
out of an array of nodes.

Currently supported node types in the array:
* String literal text (JSXText)
* Translated text via <FormattedMessage> component -> uses prop `defaultMessage`
* Translated text via {i18n.translate} call -> uses passed options object key `defaultMessage`
*/
export function getIntentFromNode(originalNode: TSESTree.JSXOpeningElement): string {
const parent = originalNode.parent as TSESTree.JSXElement;

const node = Array.isArray(parent.children) ? parent.children : [];

if (node.length === 0) {
return '';
}

/*
In order to satisfy TS we need to do quite a bit of defensive programming.
This is my best attempt at providing the minimum amount of typeguards and
keeping the code readable. In the cases where types are explicitly set to
variables, it was done to help the compiler when it couldn't infer the type.
*/
return node.reduce((acc: string, currentNode) => {
switch (currentNode.type) {
case 'JSXText':
// When node is a string primitive
return `${acc}${strip(currentNode.value)}`;

case 'JSXElement':
// Determining whether node is of form `<FormattedMessage defaultMessage="message" />`
const name: TSESTree.JSXTagNameExpression = currentNode.openingElement.name;
const attributes: Array<TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute> =
currentNode.openingElement.attributes;

if (!('name' in name) || name.name !== 'FormattedMessage') {
return '';
}

const defaultMessageProp = attributes.find(
(attribute) => 'name' in attribute && attribute.name.name === 'defaultMessage'
);

if (
!defaultMessageProp ||
!('value' in defaultMessageProp) ||
!('type' in defaultMessageProp.value!) ||
defaultMessageProp.value.type !== 'Literal' ||
typeof defaultMessageProp.value.value !== 'string'
) {
return '';
}

return `${acc}${strip(defaultMessageProp.value.value)}`;

case 'JSXExpressionContainer':
// Determining whether node is of form `{i18n.translate('foo', { defaultMessage: 'message'})}`
const expression: TSESTree.JSXEmptyExpression | TSESTree.Expression =
currentNode.expression;

if (!('arguments' in expression)) {
return '';
}

const args: TSESTree.CallExpressionArgument[] = expression.arguments;
const callee: TSESTree.LeftHandSideExpression = expression.callee;

if (!('object' in callee)) {
return '';
}

const object: TSESTree.LeftHandSideExpression = callee.object;
const property: TSESTree.Expression | TSESTree.PrivateIdentifier = callee.property;

if (!('name' in object) || !('name' in property)) {
return '';
}

if (object.name !== 'i18n' || property.name !== 'translate') {
return '';
}

const callExpressionArgument: TSESTree.CallExpressionArgument | undefined = args.find(
(arg) => arg.type === 'ObjectExpression'
);

if (!callExpressionArgument || callExpressionArgument.type !== 'ObjectExpression') {
return '';
}

const defaultMessageValue: TSESTree.ObjectLiteralElement | undefined =
callExpressionArgument.properties.find(
(prop) =>
prop.type === 'Property' && 'name' in prop.key && prop.key.name === 'defaultMessage'
);

if (
!defaultMessageValue ||
!('value' in defaultMessageValue) ||
defaultMessageValue.value.type !== 'Literal' ||
typeof defaultMessageValue.value.value !== 'string'
) {
return '';
}

return `${acc}${strip(defaultMessageValue.value.value)}`;

default:
break;
}

return acc;
}, '');
}

function strip(input: string): string {
if (!input) return '';

const cleanedString = camelCase(input);

return `${cleanedString.charAt(0).toUpperCase()}${cleanedString.slice(1)}`;
}
Loading