From 6b372b7b45913a0171958a7795b4663ea865d137 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 19 Sep 2024 15:06:30 +0300 Subject: [PATCH] [ResponseOps][Cases] Fix a bug with cases telemetry where data from other spaces are not included (#193166) ## Summary The Find SO API supports the `namespaces` parameter where you can define the spaces that the SO client should search for. If you omit the `namespaces` parameter, the SO client will use the active space. This PR creates a wrapper around the SO client to add the `namespaces: ['*']` to all Find SO usages to count telemetry on all spaces. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../telemetry/collect_telemetry_data.ts | 2 +- .../plugins/cases/server/telemetry/index.ts | 17 +++--- .../server/telemetry/queries/alerts.test.ts | 11 +++- .../queries/case_system_action.test.ts | 50 ++++++++++++++++- .../telemetry/queries/case_system_action.ts | 1 + .../server/telemetry/queries/cases.test.ts | 21 +++++++- .../cases/server/telemetry/queries/cases.ts | 19 ++++--- .../server/telemetry/queries/comments.test.ts | 14 ++++- .../telemetry/queries/configuration.test.ts | 24 +++++++-- .../server/telemetry/queries/configuration.ts | 1 + .../telemetry/queries/connectors.test.ts | 12 ++++- .../server/telemetry/queries/connectors.ts | 1 + .../queries/{pushed.test.ts => push.test.ts} | 15 ++++-- .../telemetry/queries/{pushes.ts => push.ts} | 1 + .../telemetry/queries/user_actions.test.ts | 14 ++++- .../server/telemetry/queries/utils.test.ts | 23 ++++++-- .../cases/server/telemetry/queries/utils.ts | 5 +- .../telemetry_saved_objects_client.test.ts | 30 +++++++++++ .../telemetry_saved_objects_client.ts | 24 +++++++++ .../plugins/cases/server/telemetry/types.ts | 5 +- .../common/lib/api/index.ts | 2 + .../common/lib/api/telemetry.ts | 38 +++++++++++++ .../common/plugins/cases/kibana.jsonc | 1 + .../common/plugins/cases/server/plugin.ts | 2 + .../common/plugins/cases/server/routes.ts | 29 ++++++++++ .../common/plugins/cases/tsconfig.json | 1 + .../security_and_spaces/tests/common/index.ts | 5 ++ .../tests/common/telemetry.ts | 53 +++++++++++++++++++ 28 files changed, 379 insertions(+), 42 deletions(-) rename x-pack/plugins/cases/server/telemetry/queries/{pushed.test.ts => push.test.ts} (82%) rename x-pack/plugins/cases/server/telemetry/queries/{pushes.ts => push.ts} (98%) create mode 100644 x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts create mode 100644 x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts create mode 100644 x-pack/test/cases_api_integration/common/lib/api/telemetry.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts diff --git a/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts b/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts index cabb7743a540d..1bcf599f014fb 100644 --- a/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts +++ b/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts @@ -11,7 +11,7 @@ import { getCasesSystemActionData } from './queries/case_system_action'; import { getUserCommentsTelemetryData } from './queries/comments'; import { getConfigurationTelemetryData } from './queries/configuration'; import { getConnectorsTelemetryData } from './queries/connectors'; -import { getPushedTelemetryData } from './queries/pushes'; +import { getPushedTelemetryData } from './queries/push'; import { getUserActionsTelemetryData } from './queries/user_actions'; import type { CasesTelemetry, CollectTelemetryDataParams } from './types'; diff --git a/x-pack/plugins/cases/server/telemetry/index.ts b/x-pack/plugins/cases/server/telemetry/index.ts index 5f10dcc6a3c72..c30d34d6c215c 100644 --- a/x-pack/plugins/cases/server/telemetry/index.ts +++ b/x-pack/plugins/cases/server/telemetry/index.ts @@ -5,12 +5,7 @@ * 2.0. */ -import type { - CoreSetup, - ISavedObjectsRepository, - Logger, - PluginInitializerContext, -} from '@kbn/core/server'; +import type { CoreSetup, Logger, PluginInitializerContext } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; @@ -25,6 +20,7 @@ import { } from '../../common/constants'; import type { CasesTelemetry } from './types'; import { casesSchema } from './schema'; +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; export { scheduleCasesTelemetryTask } from './schedule_telemetry_task'; @@ -42,13 +38,18 @@ export const createCasesTelemetry = ({ usageCollection, logger, }: CreateCasesTelemetryArgs) => { - const getInternalSavedObjectClient = async (): Promise => { + const getInternalSavedObjectClient = async (): Promise => { const [coreStart] = await core.getStartServices(); - return coreStart.savedObjects.createInternalRepository([ + const soClient = coreStart.savedObjects.createInternalRepository([ ...SAVED_OBJECT_TYPES, FILE_SO_TYPE, CASE_RULES_SAVED_OBJECT, ]); + + // Wrapping the internalRepository with the `TelemetrySavedObjectsClient` + // to ensure some best practices when collecting "all the telemetry" + // (i.e.: `.find` requests should query all spaces) + return new TelemetrySavedObjectsClient(soClient); }; taskManager.registerTaskDefinitions({ diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts index 0eaa99c57c0f3..11636b50ebd4e 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts @@ -7,12 +7,15 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getAlertsTelemetryData } from './alerts'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('alerts', () => { const logger = loggingSystemMock.createLogger(); describe('getAlertsTelemetryData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -35,7 +38,10 @@ describe('alerts', () => { }); it('it returns the correct res', async () => { - const res = await getAlertsTelemetryData({ savedObjectsClient, logger }); + const res = await getAlertsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -48,7 +54,7 @@ describe('alerts', () => { }); it('should call find with correct arguments', async () => { - await getAlertsTelemetryData({ savedObjectsClient, logger }); + await getAlertsTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -117,6 +123,7 @@ describe('alerts', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts index 6009d646431ed..0f121639e0f32 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts @@ -7,12 +7,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getCasesSystemActionData } from './case_system_action'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('casesSystemAction', () => { const logger = loggingSystemMock.createLogger(); describe('getCasesSystemActionData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); beforeEach(() => { jest.clearAllMocks(); @@ -26,7 +28,10 @@ describe('casesSystemAction', () => { }); it('calculates the metrics correctly', async () => { - const res = await getCasesSystemActionData({ savedObjectsClient, logger }); + const res = await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ totalCasesCreated: 4, totalRules: 2 }); }); @@ -38,8 +43,49 @@ describe('casesSystemAction', () => { page: 1, }); - const res = await getCasesSystemActionData({ savedObjectsClient, logger }); + const res = await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); + expect(res).toEqual({ totalCasesCreated: 0, totalRules: 0 }); }); + + it('should call find with correct arguments', async () => { + savedObjectsClient.find.mockResolvedValue({ + total: 1, + saved_objects: [], + per_page: 1, + page: 1, + }); + + await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); + + expect(savedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "aggs": Object { + "counterSum": Object { + "sum": Object { + "field": "cases-rules.attributes.counter", + }, + }, + "totalRules": Object { + "cardinality": Object { + "field": "cases-rules.attributes.rules.id", + }, + }, + }, + "namespaces": Array [ + "*", + ], + "page": 1, + "perPage": 1, + "type": "cases-rules", + } + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts index 0e05006e3c437..6eda6b477611c 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts @@ -26,6 +26,7 @@ export const getCasesSystemActionData = async ({ cardinality: { field: `${CASE_RULES_SAVED_OBJECT}.attributes.rules.id` }, }, }, + namespaces: ['*'], }); return { diff --git a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts index 560997e8802be..fdfe39f940e9b 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts @@ -15,6 +15,7 @@ import type { FileAttachmentAggregationResults, } from '../types'; import { getCasesTelemetryData } from './cases'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; const MOCK_FIND_TOTAL = 5; const SOLUTION_TOTAL = 1; @@ -23,6 +24,7 @@ describe('getCasesTelemetryData', () => { describe('getCasesTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); const mockFind = (aggs: object, so: SavedObjectsFindResponse['saved_objects'] = []) => { savedObjectsClient.find.mockResolvedValueOnce({ @@ -322,7 +324,10 @@ describe('getCasesTelemetryData', () => { }; }; - const res = await getCasesTelemetryData({ savedObjectsClient, logger }); + const res = await getCasesTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); const allAttachmentsTotal = 5; const allAttachmentsAverage = allAttachmentsTotal / MOCK_FIND_TOTAL; @@ -406,7 +411,7 @@ describe('getCasesTelemetryData', () => { it('should call find with correct arguments', async () => { mockResponse(); - await getCasesTelemetryData({ savedObjectsClient, logger }); + await getCasesTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` Object { @@ -660,6 +665,9 @@ describe('getCasesTelemetryData', () => { }, }, }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "cases", @@ -974,6 +982,9 @@ describe('getCasesTelemetryData', () => { }, }, }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "cases-comments", @@ -1023,6 +1034,7 @@ describe('getCasesTelemetryData', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); expect(savedObjectsClient.find.mock.calls[3][0]).toEqual({ @@ -1068,6 +1080,7 @@ describe('getCasesTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); for (const [index, sortField] of ['created_at', 'updated_at', 'closed_at'].entries()) { @@ -1079,6 +1092,7 @@ describe('getCasesTelemetryData', () => { sortField, sortOrder: 'desc', type: 'cases', + namespaces: ['*'], }); } @@ -1172,6 +1186,9 @@ describe('getCasesTelemetryData', () => { "function": "is", "type": "function", }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "file", diff --git a/x-pack/plugins/cases/server/telemetry/queries/cases.ts b/x-pack/plugins/cases/server/telemetry/queries/cases.ts index abd1979d752e8..81eefd6af1d1d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/cases.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/cases.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ISavedObjectsRepository, SavedObjectsFindResponse } from '@kbn/core/server'; +import type { SavedObjectsFindResponse } from '@kbn/core/server'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { fromKueryExpression } from '@kbn/es-query'; import { @@ -37,6 +37,7 @@ import { } from './utils'; import type { CasePersistedAttributes } from '../../common/types/case'; import { CasePersistedStatus } from '../../common/types/case'; +import type { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; export const getLatestCasesDates = async ({ savedObjectsClient, @@ -48,6 +49,7 @@ export const getLatestCasesDates = async ({ sortField, sortOrder: 'desc', type: CASE_SAVED_OBJECT, + namespaces: ['*'], }); const savedObjects = await Promise.all([ @@ -145,7 +147,7 @@ export const getCasesTelemetryData = async ({ }; const getCasesSavedObjectTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const caseByOwnerAggregationQuery = OWNERS.reduce( (aggQuery, owner) => ({ @@ -169,6 +171,7 @@ const getCasesSavedObjectTelemetry = async ( page: 0, perPage: 0, type: CASE_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...caseByOwnerAggregationQuery, ...getCountsAggregationQuery(CASE_SAVED_OBJECT), @@ -231,7 +234,7 @@ const getAssigneesAggregations = () => ({ }); const getCommentsSavedObjectTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const attachmentRegistries = () => ({ externalReferenceTypes: { @@ -275,6 +278,7 @@ const getCommentsSavedObjectTelemetry = async ( page: 0, perPage: 0, type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...attachmentsByOwnerAggregationQuery, ...attachmentRegistries(), @@ -288,7 +292,7 @@ const getCommentsSavedObjectTelemetry = async ( }; const getFilesTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const averageSize = () => ({ averageSize: { @@ -332,17 +336,19 @@ const getFilesTelemetry = async ( perPage: 0, type: FILE_SO_TYPE, filter: filterCaseIdExists, + namespaces: ['*'], aggs: { ...filesByOwnerAggregationQuery, ...averageSize(), ...top20MimeTypes() }, }); }; const getAlertsTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { return savedObjectsClient.find({ page: 0, perPage: 0, type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], filter: getOnlyAlertsCommentsFilter(), aggs: { ...getReferencesAggregationQuery({ @@ -355,12 +361,13 @@ const getAlertsTelemetry = async ( }; const getConnectorsTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { return savedObjectsClient.find({ page: 0, perPage: 0, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], filter: getOnlyConnectorsFilter(), aggs: { ...getReferencesAggregationQuery({ diff --git a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts index 9eed9b4040992..d3104bd9a79ad 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts @@ -7,11 +7,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getUserCommentsTelemetryData } from './comments'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('comments', () => { describe('getUserCommentsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -34,7 +37,10 @@ describe('comments', () => { }); it('it returns the correct res', async () => { - const res = await getUserCommentsTelemetryData({ savedObjectsClient, logger }); + const res = await getUserCommentsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -47,7 +53,10 @@ describe('comments', () => { }); it('should call find with correct arguments', async () => { - await getUserCommentsTelemetryData({ savedObjectsClient, logger }); + await getUserCommentsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -116,6 +125,7 @@ describe('comments', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts b/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts index 57c7c067a13cf..7e69c60980db1 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts @@ -8,11 +8,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { CustomFieldTypes } from '../../../common/types/domain'; import { getConfigurationTelemetryData } from './configuration'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('configuration', () => { describe('getConfigurationTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -66,7 +69,10 @@ describe('configuration', () => { }); it('it returns the correct res', async () => { - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { @@ -82,7 +88,10 @@ describe('configuration', () => { }); it('should call find with correct arguments', async () => { - await getConfigurationTelemetryData({ savedObjectsClient, logger }); + await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { closureType: { @@ -95,6 +104,7 @@ describe('configuration', () => { page: 1, perPage: 5, type: 'cases-configure', + namespaces: ['*'], }); }); @@ -135,7 +145,10 @@ describe('configuration', () => { }, }); - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { @@ -205,7 +218,10 @@ describe('configuration', () => { }, }); - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { diff --git a/x-pack/plugins/cases/server/telemetry/queries/configuration.ts b/x-pack/plugins/cases/server/telemetry/queries/configuration.ts index e3aff3216f5d5..6b736761207c8 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/configuration.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/configuration.ts @@ -28,6 +28,7 @@ export const getConfigurationTelemetryData = async ({ page: 1, perPage: 5, type: CASE_CONFIGURE_SAVED_OBJECT, + namespaces: ['*'], aggs: { closureType: { terms: { field: `${CASE_CONFIGURE_SAVED_OBJECT}.attributes.closure_type` }, diff --git a/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts b/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts index 684c77bac159a..03779f8714d8d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts @@ -7,11 +7,13 @@ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { getConnectorsTelemetryData } from './connectors'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('getConnectorsTelemetryData', () => { describe('getConnectorsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); const mockFind = (aggs: Record) => { savedObjectsClient.find.mockResolvedValueOnce({ @@ -42,7 +44,10 @@ describe('getConnectorsTelemetryData', () => { it('it returns the correct res', async () => { mockResponse(); - const res = await getConnectorsTelemetryData({ savedObjectsClient, logger }); + const res = await getConnectorsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { all: { @@ -71,7 +76,7 @@ describe('getConnectorsTelemetryData', () => { it('should call find with correct arguments', async () => { mockResponse(); - await getConnectorsTelemetryData({ savedObjectsClient, logger }); + await getConnectorsTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find.mock.calls[0][0]).toEqual({ aggs: { @@ -101,6 +106,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); expect(savedObjectsClient.find.mock.calls[1][0]).toEqual({ @@ -151,6 +157,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); for (const [index, connector] of [ @@ -205,6 +212,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); } }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/connectors.ts b/x-pack/plugins/cases/server/telemetry/queries/connectors.ts index 0e8b12e1ed192..c3f254fadb4ce 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/connectors.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/connectors.ts @@ -37,6 +37,7 @@ export const getConnectorsTelemetryData = async ({ perPage: 0, filter, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...aggs, }, diff --git a/x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts b/x-pack/plugins/cases/server/telemetry/queries/push.test.ts similarity index 82% rename from x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts rename to x-pack/plugins/cases/server/telemetry/queries/push.test.ts index e25718f0feac9..1834c5f5d54c0 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/push.test.ts @@ -6,12 +6,15 @@ */ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { getPushedTelemetryData } from './pushes'; +import { getPushedTelemetryData } from './push'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; -describe('pushes', () => { +describe('push', () => { describe('getPushedTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -27,7 +30,10 @@ describe('pushes', () => { }); it('it returns the correct res', async () => { - const res = await getPushedTelemetryData({ savedObjectsClient, logger }); + const res = await getPushedTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { maxOnACase: 1, @@ -37,7 +43,7 @@ describe('pushes', () => { }); it('should call find with correct arguments', async () => { - await getPushedTelemetryData({ savedObjectsClient, logger }); + await getPushedTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { references: { @@ -86,6 +92,7 @@ describe('pushes', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/pushes.ts b/x-pack/plugins/cases/server/telemetry/queries/push.ts similarity index 98% rename from x-pack/plugins/cases/server/telemetry/queries/pushes.ts rename to x-pack/plugins/cases/server/telemetry/queries/push.ts index 0462a7ff0ef13..ea1127ae4520b 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/pushes.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/push.ts @@ -29,6 +29,7 @@ export const getPushedTelemetryData = async ({ perPage: 0, filter: pushFilter, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...getMaxBucketOnCaseAggregationQuery(CASE_USER_ACTION_SAVED_OBJECT) }, }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts index c01c8d329c5b0..b6c45d8da3efc 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts @@ -7,11 +7,14 @@ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { getUserActionsTelemetryData } from './user_actions'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('user_actions', () => { describe('getUserActionsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -34,7 +37,10 @@ describe('user_actions', () => { }); it('it returns the correct res', async () => { - const res = await getUserActionsTelemetryData({ savedObjectsClient, logger }); + const res = await getUserActionsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -47,7 +53,10 @@ describe('user_actions', () => { }); it('should call find with correct arguments', async () => { - await getUserActionsTelemetryData({ savedObjectsClient, logger }); + await getUserActionsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -101,6 +110,7 @@ describe('user_actions', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index bf975b84f46c5..6c66c5aab81c7 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -29,6 +29,7 @@ import { getReferencesAggregationQuery, getSolutionValues, } from './utils'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('utils', () => { describe('getSolutionValues', () => { @@ -1017,7 +1018,12 @@ describe('utils', () => { }); it('returns the correct counts and max data', async () => { - const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + const res = await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); expect(res).toEqual({ all: { total: 5, @@ -1030,6 +1036,7 @@ describe('utils', () => { }); it('returns zero data if the response aggregation is not as expected', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -1037,7 +1044,10 @@ describe('utils', () => { page: 1, }); - const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const res = await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); expect(res).toEqual({ all: { total: 5, @@ -1050,7 +1060,13 @@ describe('utils', () => { }); it('should call find with correct arguments', async () => { - await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); + expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -1104,6 +1120,7 @@ describe('utils', () => { page: 0, perPage: 0, type: 'test', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index ff785077d74ac..65b81e3362300 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -7,7 +7,6 @@ import { get } from 'lodash'; import type { KueryNode } from '@kbn/es-query'; -import type { ISavedObjectsRepository } from '@kbn/core/server'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -32,6 +31,7 @@ import type { import { buildFilter } from '../../client/utils'; import type { Owner } from '../../../common/constants/types'; import type { ConfigurationPersistedAttributes } from '../../common/types/configure'; +import type { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; export const getCountsAggregationQuery = (savedObjectType: string) => ({ counts: { @@ -126,7 +126,7 @@ export const getCountsAndMaxData = async ({ savedObjectType, filter, }: { - savedObjectsClient: ISavedObjectsRepository; + savedObjectsClient: TelemetrySavedObjectsClient; savedObjectType: string; filter?: KueryNode; }) => { @@ -138,6 +138,7 @@ export const getCountsAndMaxData = async ({ perPage: 0, filter, type: savedObjectType, + namespaces: ['*'], aggs: { ...getCountsAggregationQuery(savedObjectType), ...getMaxBucketOnCaseAggregationQuery(savedObjectType), diff --git a/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts new file mode 100644 index 0000000000000..bbe2d58a1ce9b --- /dev/null +++ b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts @@ -0,0 +1,30 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; +import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; + +describe('TelemetrySavedObjectsClient', () => { + it("find requests are extended with `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + + await telemetrySavedObjectsClient.find({ type: 'my-test-type' }); + expect(savedObjectsRepository.find).toBeCalledWith({ type: 'my-test-type', namespaces: ['*'] }); + }); + + it("allow callers to overwrite the `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + + await telemetrySavedObjectsClient.find({ type: 'my-test-type', namespaces: ['some_space'] }); + expect(savedObjectsRepository.find).toBeCalledWith({ + type: 'my-test-type', + namespaces: ['some_space'], + }); + }); +}); diff --git a/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts new file mode 100644 index 0000000000000..42ae1fdd296d4 --- /dev/null +++ b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts @@ -0,0 +1,24 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsFindOptions, SavedObjectsFindResponse } from '@kbn/core/server'; +import { SavedObjectsClient } from '@kbn/core/server'; + +/** + * Extends the SavedObjectsClient to fit the telemetry fetching requirements (i.e.: find objects from all namespaces by default) + */ +export class TelemetrySavedObjectsClient extends SavedObjectsClient { + /** + * Find the SavedObjects matching the search query in all the Spaces by default + * @param options + */ + async find( + options: SavedObjectsFindOptions + ): Promise> { + return super.find({ namespaces: ['*'], ...options }); + } +} diff --git a/x-pack/plugins/cases/server/telemetry/types.ts b/x-pack/plugins/cases/server/telemetry/types.ts index 294efdbce1125..b4996da27f234 100644 --- a/x-pack/plugins/cases/server/telemetry/types.ts +++ b/x-pack/plugins/cases/server/telemetry/types.ts @@ -5,9 +5,10 @@ * 2.0. */ -import type { ISavedObjectsRepository, Logger } from '@kbn/core/server'; +import type { Logger } from '@kbn/core/server'; import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; import type { Owner } from '../../common/constants/types'; +import type { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; export type BucketKeyString = Omit & { key: string }; @@ -35,7 +36,7 @@ export interface ReferencesAggregation { } export interface CollectTelemetryDataParams { - savedObjectsClient: ISavedObjectsRepository; + savedObjectsClient: TelemetrySavedObjectsClient; logger: Logger; } diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts index cfb0596fa1ce9..ea0f66affdc35 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts @@ -61,6 +61,8 @@ export * from './user_profiles'; export * from './omit'; export * from './configuration'; export * from './files'; +export * from './telemetry'; + export { getSpaceUrlPrefix } from './helpers'; function toArray(input: T | T[]): T[] { diff --git a/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts b/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts new file mode 100644 index 0000000000000..785c059249030 --- /dev/null +++ b/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts @@ -0,0 +1,38 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import { CasesTelemetry } from '@kbn/cases-plugin/server/telemetry/types'; +import { CASES_TELEMETRY_TASK_NAME } from '@kbn/cases-plugin/common/constants'; + +interface CasesTelemetryPayload { + stats: { stack_stats: { kibana: { plugins: { cases: CasesTelemetry } } } }; +} + +export const getTelemetry = async (supertest: SuperTest.Agent): Promise => { + const { body } = await supertest + .post('/internal/telemetry/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ unencrypted: true, refreshCache: true }) + .expect(200); + + return body[0]; +}; + +export const runTelemetryTask = async (supertest: SuperTest.Agent) => { + await supertest + .post('/api/cases_fixture/telemetry/run_soon') + .set('kbn-xsrf', 'xxx') + .send({ taskId: CASES_TELEMETRY_TASK_NAME }) + .expect(200); +}; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc index 135db481efeef..91238eae39223 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc +++ b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc @@ -11,6 +11,7 @@ "features", "cases", "files", + "taskManager" ], "optionalPlugins": [ "security", diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts index 488b56927450f..a10bf8ed1797e 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts @@ -13,6 +13,7 @@ import type { CasesServerStart, CasesServerSetup } from '@kbn/cases-plugin/serve import { FilesSetup } from '@kbn/files-plugin/server'; import { PluginStartContract as ActionsPluginsStart } from '@kbn/actions-plugin/server/plugin'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { getPersistableStateAttachment } from './attachments/persistable_state'; import { getExternalReferenceAttachment } from './attachments/external_reference'; import { registerRoutes } from './routes'; @@ -29,6 +30,7 @@ export interface FixtureStartDeps { security?: SecurityPluginStart; spaces?: SpacesPluginStart; cases: CasesServerStart; + taskManager: TaskManagerStartContract; } export class FixturePlugin implements Plugin { diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts index 11335c4d7adc7..10139f636c809 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts @@ -15,6 +15,7 @@ import type { } from '@kbn/cases-plugin/server/attachment_framework/types'; import { BulkCreateCasesRequest, CasesPatchRequest } from '@kbn/cases-plugin/common/types/api'; import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types'; +import { CASES_TELEMETRY_TASK_NAME } from '@kbn/cases-plugin/common/constants'; import type { FixtureStartDeps } from './plugin'; const hashParts = (parts: string[]): string => { @@ -178,4 +179,32 @@ export const registerRoutes = (core: CoreSetup, logger: Logger } } ); + + router.post( + { + path: '/api/cases_fixture/telemetry/run_soon', + validate: { + body: schema.object({ + taskId: schema.string({ + validate: (telemetryTaskId: string) => { + if (CASES_TELEMETRY_TASK_NAME === telemetryTaskId) { + return; + } + + return 'invalid telemetry task id'; + }, + }), + }), + }, + }, + async (context, req, res) => { + const { taskId } = req.body; + try { + const [_, { taskManager }] = await core.getStartServices(); + return res.ok({ body: await taskManager.runSoon(taskId) }); + } catch (err) { + return res.ok({ body: { id: taskId, error: `${err}` } }); + } + } + ); }; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json b/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json index 0e0443d2930e9..72a20bd3f40d4 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json +++ b/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json @@ -18,6 +18,7 @@ "@kbn/features-plugin", "@kbn/spaces-plugin", "@kbn/security-plugin", + "@kbn/task-manager-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index e731e0101bdc0..f9360e473080d 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -70,6 +70,11 @@ export default ({ loadTestFile }: FtrProviderContext): void => { */ loadTestFile(require.resolve('./cases/bulk_create_cases')); + /** + * Telemetry + */ + loadTestFile(require.resolve('./telemetry')); + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces // which causes errors in any tests after them that relies on those }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts new file mode 100644 index 0000000000000..0c47e62fae79c --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts @@ -0,0 +1,53 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { getPostCaseRequest } from '../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + getTelemetry, + runTelemetryTask, +} from '../../../common/lib/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { superUser } from '../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + + describe('Cases telemetry', () => { + before(async () => { + await deleteAllCaseItems(es); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should count cases from all spaces', async () => { + await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: 'space2', + }); + + await runTelemetryTask(supertest); + + await retry.try(async () => { + const res = await getTelemetry(supertest); + expect(res.stats.stack_stats.kibana.plugins.cases.cases.all.total).toBe(2); + }); + }); + }); +};