[8.x] [kbn-scout] Scout reporter updates (#206431) (#208651)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[kbn-scout] Scout reporter updates
(#206431)](https://github.com/elastic/kibana/pull/206431)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"David
Olaru","email":"dolaru@elastic.co"},"sourceCommit":{"committedDate":"2025-01-28T23:08:37Z","message":"[kbn-scout]
Scout reporter updates (#206431)\n\n## Summary\r\n\r\n- Centralized
Scout reporter settings\r\n- Added owner area and config/test file
information to reporter events\r\n- Attempt to upload events at the end
of a test run\r\n- Enable Scout reporter test events upload for the
`pull request` and\r\n`on merge`
pipelines","sha":"fd7053b319f3df2820e3e1879092703635bbc3ae","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor"],"title":"[kbn-scout]
Scout reporter
updates","number":206431,"url":"https://github.com/elastic/kibana/pull/206431","mergeCommit":{"message":"[kbn-scout]
Scout reporter updates (#206431)\n\n## Summary\r\n\r\n- Centralized
Scout reporter settings\r\n- Added owner area and config/test file
information to reporter events\r\n- Attempt to upload events at the end
of a test run\r\n- Enable Scout reporter test events upload for the
`pull request` and\r\n`on merge`
pipelines","sha":"fd7053b319f3df2820e3e1879092703635bbc3ae"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206431","number":206431,"mergeCommit":{"message":"[kbn-scout]
Scout reporter updates (#206431)\n\n## Summary\r\n\r\n- Centralized
Scout reporter settings\r\n- Added owner area and config/test file
information to reporter events\r\n- Attempt to upload events at the end
of a test run\r\n- Enable Scout reporter test events upload for the
`pull request` and\r\n`on merge`
pipelines","sha":"fd7053b319f3df2820e3e1879092703635bbc3ae"}}]}]
BACKPORT-->

Co-authored-by: David Olaru <dolaru@elastic.co>
This commit is contained in:
Kibana Machine 2025-01-29 23:50:25 +11:00 committed by GitHub
parent 37bb66473b
commit 96a4c33070
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 287 additions and 111 deletions

View file

@ -24,6 +24,7 @@ spec:
GITHUB_COMMIT_STATUS_CONTEXT: buildkite/on-merge
REPORT_FAILED_TESTS_TO_GITHUB: 'true'
ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true'
SCOUT_REPORTER_ENABLED: 'true'
allow_rebuilds: true
branch_configuration: main 7.17 8.15
default_branch: main

View file

@ -23,6 +23,7 @@ spec:
ELASTIC_GITHUB_BUILD_COMMIT_STATUS_ENABLED: 'true'
ELASTIC_GITHUB_STEP_COMMIT_STATUS_ENABLED: 'true'
GITHUB_BUILD_COMMIT_STATUS_CONTEXT: kibana-ci
SCOUT_REPORTER_ENABLED: 'true'
allow_rebuilds: true
branch_configuration: ''
cancel_intermediate_builds: true

View file

@ -167,6 +167,17 @@ EOF
export TEST_FAILURES_ES_PASSWORD
}
# Scout reporter settings
{
export SCOUT_REPORTER_ENABLED="${SCOUT_REPORTER_ENABLED:-false}"
SCOUT_REPORTER_ES_URL="$(vault_get scout/reporter/cluster-credentials es-url)"
export SCOUT_REPORTER_ES_URL
SCOUT_REPORTER_ES_API_KEY="$(vault_get scout/reporter/cluster-credentials es-api-key)"
export SCOUT_REPORTER_ES_API_KEY
}
# Setup Bazel Remote/Local Cache Credentials
{
BAZEL_LOCAL_DEV_CACHE_CREDENTIALS_FILE="$HOME/.kibana-ci-bazel-remote-cache-local-dev.json"

View file

@ -7,6 +7,22 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export const SCOUT_TEST_EVENTS_TEMPLATE_NAME = 'scout-test-events';
export const SCOUT_TEST_EVENTS_INDEX_PATTERN = `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-*`;
export const SCOUT_TEST_EVENTS_DATA_STREAM_NAME = `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-kibana`;
function booleanFromEnv(varName: string, defaultValue: boolean = false): boolean {
const envValue = process.env[varName];
if (envValue === undefined || envValue.trim().length === 0) return defaultValue;
return ['1', 'yes', 'true'].includes(envValue.trim().toLowerCase());
}
export const SCOUT_REPORTER_ENABLED = booleanFromEnv('SCOUT_REPORTER_ENABLED');
export const SCOUT_REPORTER_ES_URL = process.env.SCOUT_REPORTER_ES_URL;
export const SCOUT_REPORTER_ES_API_KEY = process.env.SCOUT_REPORTER_ES_API_KEY;
export const SCOUT_REPORTER_ES_VERIFY_CERTS = booleanFromEnv(
'SCOUT_REPORTER_ES_VERIFY_CERTS',
true
);
export const SCOUT_TEST_EVENTS_TEMPLATE_NAME =
process.env.SCOUT_TEST_EVENTS_TEMPLATE_NAME || 'scout-test-events';
export const SCOUT_TEST_EVENTS_INDEX_PATTERN =
process.env.SCOUT_TEST_EVENTS_INDEX_PATTERN || `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-*`;
export const SCOUT_TEST_EVENTS_DATA_STREAM_NAME =
process.env.SCOUT_TEST_EVENTS_DATA_STREAM_NAME || `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-kibana`;

View file

@ -9,4 +9,5 @@
export * as cli from './src/cli';
export * as datasources from './src/datasources';
export * from './src/helpers';
export * from './src/reporting';

View file

@ -8,8 +8,13 @@
*/
import { Command } from '@kbn/dev-cli-runner';
import {
SCOUT_REPORTER_ES_API_KEY,
SCOUT_REPORTER_ES_URL,
SCOUT_REPORTER_ES_VERIFY_CERTS,
} from '@kbn/scout-info';
import { ScoutReportDataStream } from '../reporting/report/events';
import { getValidatedESClient } from './common';
import { getValidatedESClient } from '../helpers/elasticsearch';
export const initializeReportDatastream: Command<void> = {
name: 'initialize-report-datastream',
@ -18,13 +23,14 @@ export const initializeReportDatastream: Command<void> = {
string: ['esURL', 'esAPIKey'],
boolean: ['verifyTLSCerts'],
default: {
esURL: process.env.ES_URL,
esAPIKey: process.env.ES_API_KEY,
esURL: SCOUT_REPORTER_ES_URL,
esAPIKey: SCOUT_REPORTER_ES_API_KEY,
verifyTLSCerts: SCOUT_REPORTER_ES_VERIFY_CERTS,
},
help: `
--esURL (required) Elasticsearch URL [env: ES_URL]
--esAPIKey (required) Elasticsearch API Key [env: ES_API_KEY]
--verifyTLSCerts (optional) Verify TLS certificates
--esURL (required) Elasticsearch URL [env: SCOUT_REPORTER_ES_URL]
--esAPIKey (required) Elasticsearch API Key [env: SCOUT_REPORTER_ES_API_KEY]
--verifyTLSCerts (optional) Verify TLS certificates [env: SCOUT_REPORTER_ES_VERIFY_CERTS]
`,
},
run: async ({ flagsReader, log }) => {
@ -41,7 +47,7 @@ export const initializeReportDatastream: Command<void> = {
rejectUnauthorized: flagsReader.boolean('verifyTLSCerts'),
},
},
log
{ log, cli: true }
);
// Initialize the report datastream

View file

@ -10,8 +10,13 @@
import fs from 'node:fs';
import { Command } from '@kbn/dev-cli-runner';
import { createFlagError } from '@kbn/dev-cli-errors';
import {
SCOUT_REPORTER_ES_URL,
SCOUT_REPORTER_ES_API_KEY,
SCOUT_REPORTER_ES_VERIFY_CERTS,
} from '@kbn/scout-info';
import { ScoutReportDataStream } from '../reporting/report/events';
import { getValidatedESClient } from './common';
import { getValidatedESClient } from '../helpers/elasticsearch';
export const uploadEvents: Command<void> = {
name: 'upload-events',
@ -20,14 +25,15 @@ export const uploadEvents: Command<void> = {
string: ['eventLogPath', 'esURL', 'esAPIKey'],
boolean: ['verifyTLSCerts'],
default: {
esURL: process.env.ES_URL,
esAPIKey: process.env.ES_API_KEY,
esURL: SCOUT_REPORTER_ES_URL,
esAPIKey: SCOUT_REPORTER_ES_API_KEY,
verifyTLSCerts: SCOUT_REPORTER_ES_VERIFY_CERTS,
},
help: `
--eventLogPath (required) Path to the event log to upload
--esURL (required) Elasticsearch URL [env: ES_URL]
--esAPIKey (required) Elasticsearch API Key [env: ES_API_KEY]
--verifyTLSCerts (optional) Verify TLS certificates
--esURL (required) Elasticsearch URL [env: SCOUT_REPORTER_ES_URL]
--esAPIKey (required) Elasticsearch API Key [env: SCOUT_REPORTER_ES_API_KEY]
--verifyTLSCerts (optional) Verify TLS certificates [env: SCOUT_REPORTER_ES_VERIFY_CERTS]
`,
},
run: async ({ flagsReader, log }) => {
@ -51,7 +57,7 @@ export const uploadEvents: Command<void> = {
rejectUnauthorized: flagsReader.boolean('verifyTLSCerts'),
},
},
log
{ log, cli: true }
);
// Event log upload

View file

@ -14,22 +14,31 @@ import { createFailError } from '@kbn/dev-cli-errors';
/**
* Get an Elasticsearch client for which connectivity has been validated
*
* @param options Elasticsearch client options
* @param log Logger instance
* @param esClientOptions Elasticsearch client options
* @param helperSettings Settings for this helper
* @param helperSettings.log Logger instance
* @param helperSettings.cli Set to `true` when invoked from a CLI context
* @throws FailError if cluster information cannot be read from the target Elasticsearch instance
*/
export async function getValidatedESClient(
options: ESClientOptions,
log: ToolingLog
esClientOptions: ESClientOptions,
helperSettings: {
log?: ToolingLog;
cli?: boolean;
}
): Promise<ESClient> {
const es = new ESClient(options);
const { log, cli = false } = helperSettings;
const es = new ESClient(esClientOptions);
await es.info().then(
(esInfo) => {
log.info(`Connected to Elasticsearch node '${esInfo.name}'`);
if (log !== undefined) {
log.info(`Connected to Elasticsearch node '${esInfo.name}'`);
}
},
(err) => {
throw createFailError(`Failed to connect to Elasticsearch\n${err}`);
const msg = `Failed to connect to Elasticsearch\n${err}`;
throw cli ? createFailError(msg) : Error(msg);
}
);

View file

@ -8,6 +8,7 @@
*/
import type { ReporterDescription } from 'playwright/test';
import { SCOUT_REPORTER_ENABLED } from '@kbn/scout-info';
import { ScoutPlaywrightReporterOptions } from './playwright/scout_playwright_reporter';
export * from './report';
@ -16,7 +17,9 @@ export * from './report';
export const scoutPlaywrightReporter = (
options?: ScoutPlaywrightReporterOptions
): ReporterDescription => {
return ['@kbn/scout-reporting/src/reporting/playwright/events', options];
return SCOUT_REPORTER_ENABLED
? ['@kbn/scout-reporting/src/reporting/playwright/events', options]
: ['null'];
};
// Playwright failed test reporting
@ -25,5 +28,3 @@ export const scoutFailedTestsReporter = (
): ReporterDescription => {
return ['@kbn/scout-reporting/src/reporting/playwright/failed_test', options];
};
export { generateTestRunId, getTestIDForTitle } from '../helpers';

View file

@ -25,10 +25,18 @@ import stripANSI from 'strip-ansi';
import { REPO_ROOT } from '@kbn/repo-info';
import {
type CodeOwnersEntry,
type CodeOwnerArea,
getCodeOwnersEntries,
getOwningTeamsForPath,
findAreaForCodeOwner,
} from '@kbn/code-owners';
import { ScoutEventsReport, ScoutReportEventAction } from '../../report';
import {
ScoutEventsReport,
ScoutFileInfo,
ScoutReportEventAction,
type ScoutTestRunInfo,
uploadScoutReportEvents,
} from '../../report';
import { environmentMetadata } from '../../../datasources';
import type { ScoutPlaywrightReporterOptions } from '../scout_playwright_reporter';
import { generateTestRunId, getTestIDForTitle } from '../../../helpers';
@ -41,6 +49,7 @@ export class ScoutPlaywrightReporter implements Reporter {
readonly name: string;
readonly runId: string;
private report: ScoutEventsReport;
private baseTestRunInfo: ScoutTestRunInfo;
private readonly codeOwnersEntries: CodeOwnersEntry[];
constructor(private reporterOptions: ScoutPlaywrightReporterOptions = {}) {
@ -54,6 +63,7 @@ export class ScoutPlaywrightReporter implements Reporter {
this.log.info(`Scout test run ID: ${this.runId}`);
this.report = new ScoutEventsReport(this.log);
this.baseTestRunInfo = { id: this.runId };
this.codeOwnersEntries = getCodeOwnersEntries();
}
@ -61,6 +71,22 @@ export class ScoutPlaywrightReporter implements Reporter {
return getOwningTeamsForPath(filePath, this.codeOwnersEntries);
}
private getOwnerAreas(owners: string[]): CodeOwnerArea[] {
return owners
.map((owner) => findAreaForCodeOwner(owner))
.filter((area) => area !== undefined) as CodeOwnerArea[];
}
private getScoutFileInfoForPath(filePath: string): ScoutFileInfo {
const fileOwners = this.getFileOwners(filePath);
return {
path: filePath,
owner: fileOwners,
area: this.getOwnerAreas(fileOwners),
};
}
/**
* Root path of this reporter's output
*/
@ -74,16 +100,29 @@ export class ScoutPlaywrightReporter implements Reporter {
return false;
}
onBegin(config: FullConfig, suite: Suite) {
onBegin(config: FullConfig, _: Suite) {
// Enrich base test run info with config file info
let configInfo: ScoutTestRunInfo['config'];
if (config.configFile !== undefined) {
configInfo = {
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, config.configFile)),
};
}
this.baseTestRunInfo = {
...this.baseTestRunInfo,
config: configInfo,
};
// Log event
this.report.logEvent({
...environmentMetadata,
reporter: {
name: this.name,
type: 'playwright',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
event: {
action: ScoutReportEventAction.RUN_BEGIN,
},
@ -98,9 +137,7 @@ export class ScoutPlaywrightReporter implements Reporter {
name: this.name,
type: 'playwright',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
suite: {
title: test.parent.titlePath().join(' '),
type: test.parent.type,
@ -111,18 +148,15 @@ export class ScoutPlaywrightReporter implements Reporter {
tags: test.tags,
annotations: test.annotations,
expected_status: test.expectedStatus,
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
},
event: {
action: ScoutReportEventAction.TEST_BEGIN,
},
file: {
path: path.relative(REPO_ROOT, test.location.file),
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
},
});
}
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
onStepBegin(test: TestCase, _: TestResult, step: TestStep) {
this.report.logEvent({
'@timestamp': step.startTime,
...environmentMetadata,
@ -130,9 +164,7 @@ export class ScoutPlaywrightReporter implements Reporter {
name: this.name,
type: 'playwright',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
suite: {
title: test.parent.titlePath().join(' '),
type: test.parent.type,
@ -147,27 +179,22 @@ export class ScoutPlaywrightReporter implements Reporter {
title: step.titlePath().join(' '),
category: step.category,
},
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
},
event: {
action: ScoutReportEventAction.TEST_STEP_BEGIN,
},
file: {
path: path.relative(REPO_ROOT, test.location.file),
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
},
});
}
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
onStepEnd(test: TestCase, _: TestResult, step: TestStep) {
this.report.logEvent({
...environmentMetadata,
reporter: {
name: this.name,
type: 'playwright',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
suite: {
title: test.parent.titlePath().join(' '),
type: test.parent.type,
@ -183,6 +210,7 @@ export class ScoutPlaywrightReporter implements Reporter {
category: step.category,
duration: step.duration,
},
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
},
event: {
action: ScoutReportEventAction.TEST_STEP_END,
@ -191,10 +219,6 @@ export class ScoutPlaywrightReporter implements Reporter {
stack_trace: step.error?.stack ? stripANSI(step.error.stack) : undefined,
},
},
file: {
path: path.relative(REPO_ROOT, test.location.file),
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
},
});
}
@ -205,9 +229,7 @@ export class ScoutPlaywrightReporter implements Reporter {
name: this.name,
type: 'playwright',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
suite: {
title: test.parent.titlePath().join(' '),
type: test.parent.type,
@ -220,6 +242,7 @@ export class ScoutPlaywrightReporter implements Reporter {
expected_status: test.expectedStatus,
status: result.status,
duration: result.duration,
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
},
event: {
action: ScoutReportEventAction.TEST_END,
@ -228,14 +251,10 @@ export class ScoutPlaywrightReporter implements Reporter {
stack_trace: result.error?.stack ? stripANSI(result.error.stack) : undefined,
},
},
file: {
path: path.relative(REPO_ROOT, test.location.file),
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
},
});
}
onEnd(result: FullResult) {
async onEnd(result: FullResult) {
this.report.logEvent({
...environmentMetadata,
reporter: {
@ -243,7 +262,7 @@ export class ScoutPlaywrightReporter implements Reporter {
type: 'playwright',
},
test_run: {
id: this.runId,
...this.baseTestRunInfo,
status: result.status,
duration: result.duration,
},
@ -252,9 +271,13 @@ export class ScoutPlaywrightReporter implements Reporter {
},
});
// Save & conclude the report
// Save, upload events & conclude the report
try {
this.report.save(this.reportRootPath);
await uploadScoutReportEvents(this.report.eventLogPath, this.log);
} catch (e) {
// Log the error but don't propagate it
this.log.error(e);
} finally {
this.report.conclude();
}
@ -271,9 +294,7 @@ export class ScoutPlaywrightReporter implements Reporter {
name: this.name,
type: 'playwright',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
event: {
action: ScoutReportEventAction.ERROR,
error: {

View file

@ -45,11 +45,23 @@ export interface ScoutReporterInfo {
type: 'jest' | 'ftr' | 'playwright';
}
/**
* Scout file info
*/
export interface ScoutFileInfo {
path: string;
owner: string | string[];
area: string | string[];
}
/**
* Scout test run info
*/
export interface ScoutTestRunInfo {
id: string;
config?: {
file?: ScoutFileInfo;
};
status?: string;
duration?: number;
}
@ -81,14 +93,7 @@ export interface ScoutTestInfo {
category?: string;
duration?: number;
};
}
/**
* Scout file info
*/
export interface ScoutFileInfo {
path: string;
owner: string | string[];
file?: ScoutFileInfo;
}
/**
@ -99,7 +104,6 @@ export interface ScoutReportEvent {
buildkite?: BuildkiteMetadata;
host?: HostMetadata;
event: ScoutReportEventInfo;
file?: ScoutFileInfo;
labels?: { [id: string]: string };
reporter: ScoutReporterInfo;
test_run: ScoutTestRunInfo;

View file

@ -48,7 +48,7 @@ export const reporterMappings: ClusterPutComponentTemplateRequest = {
export const testRunMappings: ClusterPutComponentTemplateRequest = {
name: 'scout-test-event.mappings.test-run',
version: 1,
version: 2,
template: {
mappings: {
properties: {
@ -78,7 +78,7 @@ export const suiteMappings: ClusterPutComponentTemplateRequest = {
export const testMappings: ClusterPutComponentTemplateRequest = {
name: 'scout-test-event.mappings.test',
version: 1,
version: 2,
template: {
mappings: {
properties: {

View file

@ -12,7 +12,13 @@ import path from 'node:path';
import readline from 'node:readline';
import { ToolingLog } from '@kbn/tooling-log';
import { Client as ESClient } from '@elastic/elasticsearch';
import { SCOUT_TEST_EVENTS_DATA_STREAM_NAME } from '@kbn/scout-info';
import {
SCOUT_REPORTER_ES_API_KEY,
SCOUT_REPORTER_ES_URL,
SCOUT_REPORTER_ES_VERIFY_CERTS,
SCOUT_TEST_EVENTS_DATA_STREAM_NAME,
} from '@kbn/scout-info';
import { getValidatedESClient } from '../../../../helpers/elasticsearch';
import { ScoutReportEvent } from '../event';
import * as componentTemplates from './component_templates';
import * as indexTemplates from './index_templates';
@ -141,3 +147,41 @@ export class ScoutReportDataStream {
}
}
}
/**
* Upload events logged by a Scout reporter to the configured Scout Reporter ES instance
*
* @param eventLogPath Path to event log file
* @param log Logger instance
*/
export async function uploadScoutReportEvents(eventLogPath: string, log?: ToolingLog) {
const logger = log || new ToolingLog();
const warnSettingWasNotConfigured = (settingName: string) =>
logger.warning(`Won't upload Scout reporter events: ${settingName} was not configured`);
if (SCOUT_REPORTER_ES_URL === undefined) {
warnSettingWasNotConfigured('SCOUT_REPORTER_ES_URL');
return;
}
if (SCOUT_REPORTER_ES_API_KEY === undefined) {
warnSettingWasNotConfigured('SCOUT_REPORTER_ES_API_KEY');
return;
}
log?.info(`Connecting to Scout reporter ES URL ${SCOUT_REPORTER_ES_URL}`);
const es = await getValidatedESClient(
{
node: SCOUT_REPORTER_ES_URL,
auth: { apiKey: SCOUT_REPORTER_ES_API_KEY },
tls: {
rejectUnauthorized: SCOUT_REPORTER_ES_VERIFY_CERTS,
},
},
{ log }
);
const reportDataStream = new ScoutReportDataStream(es, logger);
await reportDataStream.addEventsFromFile(eventLogPath);
}

View file

@ -95,6 +95,18 @@ export const buildkiteProperties: Record<PropertyName, MappingProperty> = {
},
};
export const fileInfoProperties: Record<PropertyName, MappingProperty> = {
path: {
type: 'keyword',
},
owner: {
type: 'keyword',
},
area: {
type: 'keyword',
},
};
export const reporterProperties: Record<PropertyName, MappingProperty> = {
name: {
type: 'text',
@ -114,6 +126,15 @@ export const testRunProperties: Record<PropertyName, MappingProperty> = {
duration: {
type: 'long',
},
config: {
type: 'object',
properties: {
file: {
type: 'object',
properties: fileInfoProperties,
},
},
},
};
export const suiteProperties: Record<PropertyName, MappingProperty> = {
@ -169,4 +190,8 @@ export const testProperties: Record<PropertyName, MappingProperty> = {
},
},
},
file: {
type: 'object',
properties: fileInfoProperties,
},
};

View file

@ -7,5 +7,5 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { ScoutEventsReport, ScoutReportEventAction } from './events';
export * from './events';
export { ScoutFailureReport, type TestFailure } from './failed_test';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info';
import { SCOUT_REPORTER_ENABLED, SCOUT_SERVERS_ROOT } from '@kbn/scout-info';
import { createPlaywrightConfig } from './create_config';
import { VALID_CONFIG_MARKER } from '../types';
import { generateTestRunId } from '@kbn/scout-reporting';
@ -48,10 +48,12 @@ describe('createPlaywrightConfig', () => {
expect(config.reporter).toEqual([
['html', { open: 'never', outputFolder: './output/reports' }],
['json', { outputFile: './output/reports/test-results.json' }],
[
'@kbn/scout-reporting/src/reporting/playwright/events',
{ name: 'scout-playwright', runId: testRunId },
],
SCOUT_REPORTER_ENABLED
? [
'@kbn/scout-reporting/src/reporting/playwright/events',
{ name: 'scout-playwright', runId: testRunId },
]
: ['null'],
[
'@kbn/scout-reporting/src/reporting/playwright/failed_test',
{ name: 'scout-playwright-failed-tests', runId: testRunId },

View file

@ -11,6 +11,7 @@ import { dirname, resolve } from 'path';
import Joi from 'joi';
import type { CustomHelpers } from 'joi';
import { SCOUT_REPORTER_ENABLED } from '@kbn/scout-info';
// valid pattern for ID
// enforced camel-case identifiers for consistency
@ -180,7 +181,7 @@ export const schema = Joi.object()
scoutReporter: Joi.object()
.keys({
enabled: Joi.boolean().default(process.env.ENABLE_SCOUT_REPORTER || false),
enabled: Joi.boolean().default(SCOUT_REPORTER_ENABLED),
})
.default(),

View file

@ -68,7 +68,7 @@ export function MochaReporterProvider({ getService }) {
}
if (config.get('scoutReporter.enabled')) {
new ScoutFTRReporter(runner);
new ScoutFTRReporter(runner, config);
}
}

View file

@ -12,18 +12,24 @@ import { ToolingLog } from '@kbn/tooling-log';
import { SCOUT_REPORT_OUTPUT_ROOT } from '@kbn/scout-info';
import { REPO_ROOT } from '@kbn/repo-info';
import {
generateTestRunId,
getTestIDForTitle,
datasources,
ScoutEventsReport,
ScoutReportEventAction,
type ScoutTestRunInfo,
generateTestRunId,
getTestIDForTitle,
uploadScoutReportEvents,
ScoutFileInfo,
} from '@kbn/scout-reporting';
import {
type CodeOwnersEntry,
type CodeOwnerArea,
getOwningTeamsForPath,
getCodeOwnersEntries,
type CodeOwnersEntry,
findAreaForCodeOwner,
} from '@kbn/code-owners';
import { Runner, Test } from '../../../fake_mocha_types';
import { Config as FTRConfig } from '../../config';
/**
* Configuration options for the Scout Mocha reporter
@ -41,9 +47,14 @@ export class ScoutFTRReporter {
readonly name: string;
readonly runId: string;
private report: ScoutEventsReport;
private readonly baseTestRunInfo: ScoutTestRunInfo;
private readonly codeOwnersEntries: CodeOwnersEntry[];
constructor(private runner: Runner, private reporterOptions: ScoutFTRReporterOptions = {}) {
constructor(
private runner: Runner,
config: FTRConfig,
private reporterOptions: ScoutFTRReporterOptions = {}
) {
this.log = new ToolingLog({
level: 'info',
writeTo: process.stdout,
@ -55,6 +66,10 @@ export class ScoutFTRReporter {
this.report = new ScoutEventsReport(this.log);
this.codeOwnersEntries = getCodeOwnersEntries();
this.baseTestRunInfo = {
id: this.runId,
config: { file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, config.path)) },
};
// Register event listeners
for (const [eventName, listener] of Object.entries({
@ -71,6 +86,22 @@ export class ScoutFTRReporter {
return getOwningTeamsForPath(filePath, this.codeOwnersEntries);
}
private getOwnerAreas(owners: string[]): CodeOwnerArea[] {
return owners
.map((owner) => findAreaForCodeOwner(owner))
.filter((area) => area !== undefined) as CodeOwnerArea[];
}
private getScoutFileInfoForPath(filePath: string): ScoutFileInfo {
const fileOwners = this.getFileOwners(filePath);
return {
path: filePath,
owner: fileOwners,
area: this.getOwnerAreas(fileOwners),
};
}
/**
* Root path of this reporter's output
*/
@ -89,9 +120,7 @@ export class ScoutFTRReporter {
name: this.name,
type: 'ftr',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
event: {
action: ScoutReportEventAction.RUN_BEGIN,
},
@ -108,9 +137,7 @@ export class ScoutFTRReporter {
name: this.name,
type: 'ftr',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
suite: {
title: test.parent?.fullTitle() || 'unknown',
type: test.parent?.root ? 'root' : 'suite',
@ -119,14 +146,13 @@ export class ScoutFTRReporter {
id: getTestIDForTitle(test.fullTitle()),
title: test.title,
tags: [],
file: test.file
? this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.file))
: undefined,
},
event: {
action: ScoutReportEventAction.TEST_BEGIN,
},
file: {
path: test.file ? path.relative(REPO_ROOT, test.file) : 'unknown',
owner: test.file ? this.getFileOwners(path.relative(REPO_ROOT, test.file)) : 'unknown',
},
});
};
@ -140,9 +166,7 @@ export class ScoutFTRReporter {
name: this.name,
type: 'ftr',
},
test_run: {
id: this.runId,
},
test_run: this.baseTestRunInfo,
suite: {
title: test.parent?.fullTitle() || 'unknown',
type: test.parent?.root ? 'root' : 'suite',
@ -151,6 +175,9 @@ export class ScoutFTRReporter {
id: getTestIDForTitle(test.fullTitle()),
title: test.title,
tags: [],
file: test.file
? this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.file))
: undefined,
status: test.isPending() ? 'skipped' : test.isPassed() ? 'passed' : 'failed',
duration: test.duration,
},
@ -161,14 +188,10 @@ export class ScoutFTRReporter {
stack_trace: test.err?.stack,
},
},
file: {
path: test.file ? path.relative(REPO_ROOT, test.file) : 'unknown',
owner: test.file ? this.getFileOwners(path.relative(REPO_ROOT, test.file)) : 'unknown',
},
});
};
onRunEnd = () => {
onRunEnd = async () => {
/**
* Root suite execution has ended
*/
@ -179,7 +202,7 @@ export class ScoutFTRReporter {
type: 'ftr',
},
test_run: {
id: this.runId,
...this.baseTestRunInfo,
status: this.runner.stats?.failures === 0 ? 'passed' : 'failed',
duration: this.runner.stats?.duration || 0,
},
@ -191,6 +214,10 @@ export class ScoutFTRReporter {
// Save & conclude the report
try {
this.report.save(this.reportRootPath);
await uploadScoutReportEvents(this.report.eventLogPath, this.log);
} catch (e) {
// Log the error but don't propagate it
this.log.error(e);
} finally {
this.report.conclude();
}