From 7cb73ebbf195c38310163bd1009e5e11ea8902c5 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 25 Nov 2020 13:41:32 +0100 Subject: [PATCH 1/8] SavedObjectsRepository.incrementCounter supports array of fields --- ...savedobjectsrepository.incrementcounter.md | 30 ++++- ...ugin-core-server.savedobjectsrepository.md | 2 +- .../lib/integration_tests/repository.test.ts | 105 ++++++++++++++++++ .../service/lib/repository.test.js | 58 +++++----- .../saved_objects/service/lib/repository.ts | 65 +++++++++-- src/core/server/server.api.md | 2 +- .../data/server/kql_telemetry/route.ts | 2 +- .../services/sample_data/usage/usage.ts | 4 +- .../server/report/store_report.test.ts | 2 +- .../server/report/store_report.ts | 2 +- .../validation_telemetry_service.ts | 2 +- .../server/collectors/lib/telemetry.ts | 2 +- .../lib/telemetry/es_ui_open_apis.test.ts | 6 +- .../lib/telemetry/es_ui_reindex_apis.test.ts | 8 +- .../lib/telemetry/es_ui_reindex_apis.ts | 8 +- 15 files changed, 237 insertions(+), 61 deletions(-) create mode 100644 src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index f3a2ee38cbdbd7..cbbe1f58ffda5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -4,12 +4,12 @@ ## SavedObjectsRepository.incrementCounter() method -Increases a counter field by one. Creates the document if one doesn't exist for the given id. +Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. Signature: ```typescript -incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; +incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise; ``` ## Parameters @@ -18,7 +18,7 @@ incrementCounter(type: string, id: string, counterFieldName: string, options?: S | --- | --- | --- | | type | string | | | id | string | | -| counterFieldName | string | | +| counterFieldNames | string[] | | | options | SavedObjectsIncrementCounterOptions | | Returns: @@ -27,3 +27,27 @@ incrementCounter(type: string, id: string, counterFieldName: string, options?: S {promise} +## Remarks + +When using incrementCounter for collecting usage data, you need to ensure that usage collection happens on a best-effort basis and doesn't negatively affect your plugin or users (see the example): - Swallow any exceptions thrown from the incrementCounter method and log a message in development. - Don't block your application on the incrementCounter method (e.g. don't use `await`) + +## Example + +Collecting usage data + +```ts +const repository = coreStart.savedObjects.createInternalRepository(); + +// NOTE: Usage collection happens on a best-effort basis, so we don't +// `await` the promise returned by `incrementCounter` and we swallow any +// exceptions in production. +repository + .incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]) + .catch((e) => (coreContext.env.cliArgs.dev ? logger.error(e) : e)); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 6a56f0bee718b6..e0a6b8af5658a0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -26,7 +26,7 @@ export declare class SavedObjectsRepository | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | -| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | +| [incrementCounter(type, id, counterFieldNames, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts new file mode 100644 index 00000000000000..67e69b32c8543b --- /dev/null +++ b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InternalCoreStart } from 'src/core/server/internal_types'; +import * as kbnTestServer from '../../../../../test_helpers/kbn_server'; +import { Root } from '../../../../root'; + +const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); +let esServer: kbnTestServer.TestElasticsearchUtils; + +describe('SavedObjectsRepository', () => { + let root: Root; + let start: InternalCoreStart; + + beforeAll(async () => { + esServer = await startES(); + root = kbnTestServer.createRootWithCorePlugins({ + server: { + basePath: '/hello', + }, + }); + + const setup = await root.setup(); + setup.savedObjects.registerType({ + hidden: false, + mappings: { + dynamic: false, + properties: {}, + }, + name: 'test_counter_type', + namespaceType: 'single', + }); + start = await root.start(); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + describe('#incrementCounter', () => { + it('initializes a new document if none exists', async () => { + const now = new Date().getTime(); + const repository = start.savedObjects.createInternalRepository(); + await repository.incrementCounter('test_counter_type', 'counter_1', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_1'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 1, + "stats.api.count2": 1, + "stats.total": 1, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + it('increments the specified counters of an existing document', async () => { + const repository = start.savedObjects.createInternalRepository(); + // Create document + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + + const now = new Date().getTime(); + // Increment counters + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_2'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 2, + "stats.api.count2": 2, + "stats.total": 2, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 6f885f17fd82b2..8443d1dd071847 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -3272,11 +3272,11 @@ describe('SavedObjectsRepository', () => { describe('#incrementCounter', () => { const type = 'config'; const id = 'one'; - const field = 'buildNum'; + const counterFields = ['buildNum', 'apiCallsCount']; const namespace = 'foo-namespace'; const originId = 'some-origin-id'; - const incrementCounterSuccess = async (type, id, field, options) => { + const incrementCounterSuccess = async (type, id, fields, options) => { const isMultiNamespace = registry.isMultiNamespace(type); if (isMultiNamespace) { const response = getMockGetResponse({ type, id }, options?.namespace); @@ -3295,7 +3295,10 @@ describe('SavedObjectsRepository', () => { type, ...mockTimestampFields, [type]: { - [field]: 8468, + ...fields.reduce((acc, field) => { + acc[field] = 8468; + return acc; + }, {}), defaultIndex: 'logstash-*', }, }, @@ -3303,25 +3306,25 @@ describe('SavedObjectsRepository', () => { }) ); - const result = await savedObjectsRepository.incrementCounter(type, id, field, options); + const result = await savedObjectsRepository.incrementCounter(type, id, fields, options); expect(client.get).toHaveBeenCalledTimes(isMultiNamespace ? 1 : 0); return result; }; describe('client calls', () => { it(`should use the ES update action if type is not multi-namespace`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledTimes(1); }); it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { - await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, counterFields, { namespace }); expect(client.get).toHaveBeenCalledTimes(1); expect(client.update).toHaveBeenCalledTimes(1); }); it(`defaults to a refresh setting of wait_for`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ refresh: 'wait_for', @@ -3331,7 +3334,7 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${namespace}:${type}:${id}`, @@ -3341,7 +3344,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, field); + await incrementCounterSuccess(type, id, counterFields); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, @@ -3351,7 +3354,7 @@ describe('SavedObjectsRepository', () => { }); it(`normalizes options.namespace from 'default' to undefined`, async () => { - await incrementCounterSuccess(type, id, field, { namespace: 'default' }); + await incrementCounterSuccess(type, id, counterFields, { namespace: 'default' }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, @@ -3361,7 +3364,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); + await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, @@ -3370,7 +3373,7 @@ describe('SavedObjectsRepository', () => { ); client.update.mockClear(); - await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, @@ -3389,7 +3392,7 @@ describe('SavedObjectsRepository', () => { it(`throws when options.namespace is '*'`, async () => { await expect( - savedObjectsRepository.incrementCounter(type, id, field, { + savedObjectsRepository.incrementCounter(type, id, counterFields, { namespace: ALL_NAMESPACES_STRING, }) ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); @@ -3398,7 +3401,7 @@ describe('SavedObjectsRepository', () => { it(`throws when type is not a string`, async () => { const test = async (type) => { await expect( - savedObjectsRepository.incrementCounter(type, id, field) + savedObjectsRepository.incrementCounter(type, id, counterFields) ).rejects.toThrowError(`"type" argument must be a string`); expect(client.update).not.toHaveBeenCalled(); }; @@ -3413,23 +3416,24 @@ describe('SavedObjectsRepository', () => { const test = async (field) => { await expect( savedObjectsRepository.incrementCounter(type, id, field) - ).rejects.toThrowError(`"counterFieldName" argument must be a string`); + ).rejects.toThrowError(`"counterFieldNames" argument must be an array of strings`); expect(client.update).not.toHaveBeenCalled(); }; - await test(null); - await test(42); - await test(false); - await test({}); + await test([null]); + await test([42]); + await test([false]); + await test([{}]); + await test([{}, false, 42, null, 'string']); }); it(`throws when type is invalid`, async () => { - await expectUnsupportedTypeError('unknownType', id, field); + await expectUnsupportedTypeError('unknownType', id, counterFields); expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { - await expectUnsupportedTypeError(HIDDEN_TYPE, id, field); + await expectUnsupportedTypeError(HIDDEN_TYPE, id, counterFields); expect(client.update).not.toHaveBeenCalled(); }); @@ -3439,7 +3443,9 @@ describe('SavedObjectsRepository', () => { elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); await expect( - savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, field, { namespace }) + savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, counterFields, { + namespace, + }) ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); expect(client.get).toHaveBeenCalledTimes(1); }); @@ -3452,8 +3458,8 @@ describe('SavedObjectsRepository', () => { it(`migrates a document and serializes the migrated doc`, async () => { const migrationVersion = mockMigrationVersion; - await incrementCounterSuccess(type, id, field, { migrationVersion }); - const attributes = { buildNum: 1 }; // this is added by the incrementCounter function + await incrementCounterSuccess(type, id, counterFields, { migrationVersion }); + const attributes = { buildNum: 1, apiCallsCount: 1 }; // this is added by the incrementCounter function const doc = { type, id, attributes, migrationVersion, ...mockTimestampFields }; expectMigrationArgs(doc); @@ -3476,6 +3482,7 @@ describe('SavedObjectsRepository', () => { ...mockTimestampFields, config: { buildNum: 8468, + apiCallsCount: 100, defaultIndex: 'logstash-*', }, originId, @@ -3487,7 +3494,7 @@ describe('SavedObjectsRepository', () => { const response = await savedObjectsRepository.incrementCounter( 'config', '6.0.0-alpha1', - 'buildNum', + ['buildNum', 'apiCallsCount'], { namespace: 'foo-namespace', } @@ -3500,6 +3507,7 @@ describe('SavedObjectsRepository', () => { version: mockVersion, attributes: { buildNum: 8468, + apiCallsCount: 100, defaultIndex: 'logstash-*', }, originId, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index d362c02de49153..57513fa40648e6 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1515,11 +1515,44 @@ export class SavedObjectsRepository { } /** - * Increases a counter field by one. Creates the document if one doesn't exist for the given id. + * Increments all the specified counter fields by one. Creates the document + * if one doesn't exist for the given id. + * + * @remarks + * When using incrementCounter for collecting usage data, you need to ensure + * that usage collection happens on a best-effort basis and doesn't + * negatively affect your plugin or users (see the example): + * - Swallow any exceptions thrown from the incrementCounter method and log + * a message in development. + * - Don't block your application on the incrementCounter method (e.g. + * don't use `await`) + * + * When supplying a field name like `stats.api.counter` the field name will + * be used as-is to create a document like: + * `{attributes: {'stats.api.counter': 1}}` + * It will not create a nested structure like: + * `{attributes: {stats: {api: {counter: 1}}}}` + * + * @example + * Collecting usage data + * ```ts + * const repository = coreStart.savedObjects.createInternalRepository(); + * + * // NOTE: Usage collection happens on a best-effort basis, so we don't + * // `await` the promise returned by `incrementCounter` and we swallow any + * // exceptions in production. + * repository + * .incrementCounter('test_counter_type', 'counter_2', [ + * 'stats.api.count', + * 'stats.api.count2', + * 'stats.total', + * ]) + * .catch((e) => (coreContext.env.cliArgs.dev ? logger.error(e) : e)); + * ``` * * @param {string} type * @param {string} id - * @param {string} counterFieldName + * @param {string} counterFieldNames * @param {object} [options={}] * @property {object} [options.migrationVersion=undefined] * @returns {promise} @@ -1527,14 +1560,17 @@ export class SavedObjectsRepository { async incrementCounter( type: string, id: string, - counterFieldName: string, + counterFieldNames: string[], options: SavedObjectsIncrementCounterOptions = {} ): Promise { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); } - if (typeof counterFieldName !== 'string') { - throw new Error('"counterFieldName" argument must be a string'); + const isArrayOfStrings = + Array.isArray(counterFieldNames) && + !counterFieldNames.some((field) => typeof field !== 'string'); + if (!isArrayOfStrings) { + throw new Error('"counterFieldNames" argument must be an array of strings'); } if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); @@ -1558,7 +1594,10 @@ export class SavedObjectsRepository { type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes: { [counterFieldName]: 1 }, + attributes: counterFieldNames.reduce((acc, counterFieldName) => { + acc[counterFieldName] = 1; + return acc; + }, {} as Record), migrationVersion, updated_at: time, }); @@ -1573,11 +1612,13 @@ export class SavedObjectsRepository { body: { script: { source: ` - if (ctx._source[params.type][params.counterFieldName] == null) { - ctx._source[params.type][params.counterFieldName] = params.count; - } - else { - ctx._source[params.type][params.counterFieldName] += params.count; + for (counterFieldName in params.counterFieldNames) { + if (ctx._source[params.type][counterFieldName] == null) { + ctx._source[params.type][counterFieldName] = params.count; + } + else { + ctx._source[params.type][counterFieldName] += params.count; + } } ctx._source.updated_at = params.time; `, @@ -1586,7 +1627,7 @@ export class SavedObjectsRepository { count: 1, time, type, - counterFieldName, + counterFieldNames, }, }, upsert: raw._source, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a03e5ec9acd275..ce04c1b5412d1d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2444,7 +2444,7 @@ export class SavedObjectsRepository { // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; + incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/plugins/data/server/kql_telemetry/route.ts b/src/plugins/data/server/kql_telemetry/route.ts index efcb3d038bcc65..c93500f360ad07 100644 --- a/src/plugins/data/server/kql_telemetry/route.ts +++ b/src/plugins/data/server/kql_telemetry/route.ts @@ -45,7 +45,7 @@ export function registerKqlTelemetryRoute( const counterName = optIn ? 'optInCount' : 'optOutCount'; try { - await internalRepository.incrementCounter('kql-telemetry', 'kql-telemetry', counterName); + await internalRepository.incrementCounter('kql-telemetry', 'kql-telemetry', [counterName]); } catch (error) { logger.warn(`Unable to increment counter: ${error}`); return response.customError({ diff --git a/src/plugins/home/server/services/sample_data/usage/usage.ts b/src/plugins/home/server/services/sample_data/usage/usage.ts index ba67906febf1a3..6a243b47dee554 100644 --- a/src/plugins/home/server/services/sample_data/usage/usage.ts +++ b/src/plugins/home/server/services/sample_data/usage/usage.ts @@ -43,7 +43,7 @@ export function usage( addInstall: async (dataSet: string) => { try { const internalRepository = await internalRepositoryPromise; - await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `installCount`); + await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, [`installCount`]); } catch (err) { handleIncrementError(err); } @@ -51,7 +51,7 @@ export function usage( addUninstall: async (dataSet: string) => { try { const internalRepository = await internalRepositoryPromise; - await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `unInstallCount`); + await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, [`unInstallCount`]); } catch (err) { handleIncrementError(err); } diff --git a/src/plugins/usage_collection/server/report/store_report.test.ts b/src/plugins/usage_collection/server/report/store_report.test.ts index d8327eb834e12c..939c37764ab0e9 100644 --- a/src/plugins/usage_collection/server/report/store_report.test.ts +++ b/src/plugins/usage_collection/server/report/store_report.test.ts @@ -69,7 +69,7 @@ describe('store_report', () => { expect(savedObjectClient.incrementCounter).toHaveBeenCalledWith( 'ui-metric', 'test-app-name:test-event-name', - 'count' + ['count'] ); expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith([ { diff --git a/src/plugins/usage_collection/server/report/store_report.ts b/src/plugins/usage_collection/server/report/store_report.ts index d9aac23fd1ff09..a54d3d226d7367 100644 --- a/src/plugins/usage_collection/server/report/store_report.ts +++ b/src/plugins/usage_collection/server/report/store_report.ts @@ -50,7 +50,7 @@ export async function storeReport( const savedObjectId = `${appName}:${eventName}`; return { saved_objects: [ - await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'), + await internalRepository.incrementCounter('ui-metric', savedObjectId, ['count']), ], }; }), diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index 0969174c7143c0..46f46eaa3026f3 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -83,7 +83,7 @@ export class ValidationTelemetryService implements Plugin { expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.overview` + [`ui_open.overview`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.cluster` + [`ui_open.cluster`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.indices` + [`ui_open.indices`] ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts index 31e4e3f07b5de4..c157d8860de12a 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts @@ -28,22 +28,22 @@ describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.close` + [`ui_reindex.close`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.open` + [`ui_reindex.open`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.start` + [`ui_reindex.start`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.stop` + [`ui_reindex.stop`] ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts index 0aaaf63196d67f..4c57b586a46cd6 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts @@ -23,11 +23,9 @@ async function incrementUIReindexOptionCounter({ }: IncrementUIReindexOptionDependencies) { const internalRepository = savedObjects.createInternalRepository(); - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.${uiReindexOptionCounter}` - ); + await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ + `ui_reindex.${uiReindexOptionCounter}`, + ]); } type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; From 8ba65bc800f86970b0189f955d0b3af804b21f3e Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 25 Nov 2020 15:14:11 +0100 Subject: [PATCH 2/8] Fix TS errors --- .../server/lib/telemetry/es_ui_open_apis.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts index 64e9b0f217555b..45cae937fb4660 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts @@ -23,11 +23,9 @@ async function incrementUIOpenOptionCounter({ }: IncrementUIOpenDependencies) { const internalRepository = savedObjects.createInternalRepository(); - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_open.${uiOpenOptionCounter}` - ); + await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ + `ui_open.${uiOpenOptionCounter}`, + ]); } type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; From 5e2c7f3492593ef71f9f577fec04a060099fad6c Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 25 Nov 2020 15:54:30 +0100 Subject: [PATCH 3/8] Fix failing test --- .../enterprise_search/server/collectors/lib/telemetry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts index 6cf0be9fd1f313..b4b7a489c6ea35 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts @@ -61,7 +61,7 @@ describe('Telemetry helpers', () => { expect(incrementCounterMock).toHaveBeenCalledWith( 'app_search_telemetry', 'app_search_telemetry', - 'ui_clicked.button' + ['ui_clicked.button'] ); expect(response).toEqual({ success: true }); }); From 7643c19d0c3b8e6ae88ef8f4f958b22ec301e8c4 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Thu, 26 Nov 2020 15:34:44 +0100 Subject: [PATCH 4/8] Ensure all the remarks make it into our documentation --- ...lugin-core-server.savedobjectsrepository.incrementcounter.md | 2 +- src/core/server/saved_objects/service/lib/repository.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index cbbe1f58ffda5b..b657ee7343c5e2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -29,7 +29,7 @@ incrementCounter(type: string, id: string, counterFieldNames: string[], options? ## Remarks -When using incrementCounter for collecting usage data, you need to ensure that usage collection happens on a best-effort basis and doesn't negatively affect your plugin or users (see the example): - Swallow any exceptions thrown from the incrementCounter method and log a message in development. - Don't block your application on the incrementCounter method (e.g. don't use `await`) +When supplying a field name like `stats.api.counter` the field name will be used as-is to create a document like: `{attributes: {'stats.api.counter': 1}}` It will not create a nested structure like: `{attributes: {stats: {api: {counter: 1}}}}` ## Example diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 57513fa40648e6..9fb462e6f489a1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1526,7 +1526,7 @@ export class SavedObjectsRepository { * a message in development. * - Don't block your application on the incrementCounter method (e.g. * don't use `await`) - * + * @remarks * When supplying a field name like `stats.api.counter` the field name will * be used as-is to create a document like: * `{attributes: {'stats.api.counter': 1}}` From 8f0a3aed286220a153e1abba8c02475af6bef57c Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 30 Nov 2020 15:47:21 +0100 Subject: [PATCH 5/8] SavedObjectsRepository.incrementCounter initialize option --- .../lib/integration_tests/repository.test.ts | 129 ++++++++++++------ .../saved_objects/service/lib/repository.ts | 8 +- 2 files changed, 93 insertions(+), 44 deletions(-) diff --git a/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts index 67e69b32c8543b..2f64776501df06 100644 --- a/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts +++ b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts @@ -57,49 +57,96 @@ describe('SavedObjectsRepository', () => { }); describe('#incrementCounter', () => { - it('initializes a new document if none exists', async () => { - const now = new Date().getTime(); - const repository = start.savedObjects.createInternalRepository(); - await repository.incrementCounter('test_counter_type', 'counter_1', [ - 'stats.api.count', - 'stats.api.count2', - 'stats.total', - ]); - const result = await repository.get('test_counter_type', 'counter_1'); - expect(result.attributes).toMatchInlineSnapshot(` - Object { - "stats.api.count": 1, - "stats.api.count2": 1, - "stats.total": 1, - } - `); - expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + describe('initialize=false', () => { + it('creates a new document if none exists and sets all counter fields set to 1', async () => { + const now = new Date().getTime(); + const repository = start.savedObjects.createInternalRepository(); + await repository.incrementCounter('test_counter_type', 'counter_1', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_1'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 1, + "stats.api.count2": 1, + "stats.total": 1, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + it('increments the specified counters of an existing document', async () => { + const repository = start.savedObjects.createInternalRepository(); + // Create document + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + + const now = new Date().getTime(); + // Increment counters + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_2'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 2, + "stats.api.count2": 2, + "stats.total": 2, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); }); - it('increments the specified counters of an existing document', async () => { - const repository = start.savedObjects.createInternalRepository(); - // Create document - await repository.incrementCounter('test_counter_type', 'counter_2', [ - 'stats.api.count', - 'stats.api.count2', - 'stats.total', - ]); + describe('initialize=true', () => { + it('creates a new document if none exists and sets all counter fields to 0', async () => { + const now = new Date().getTime(); + const repository = start.savedObjects.createInternalRepository(); + await repository.incrementCounter( + 'test_counter_type', + 'counter_3', + ['stats.api.count', 'stats.api.count2', 'stats.total'], + { initialize: true } + ); + const result = await repository.get('test_counter_type', 'counter_3'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 0, + "stats.api.count2": 0, + "stats.total": 0, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + it('sets any undefined counter fields to 0 but does not alter existing fields of an existing document', async () => { + const repository = start.savedObjects.createInternalRepository(); + // Create document + await repository.incrementCounter('test_counter_type', 'counter_4', [ + 'stats.existing_field', + ]); - const now = new Date().getTime(); - // Increment counters - await repository.incrementCounter('test_counter_type', 'counter_2', [ - 'stats.api.count', - 'stats.api.count2', - 'stats.total', - ]); - const result = await repository.get('test_counter_type', 'counter_2'); - expect(result.attributes).toMatchInlineSnapshot(` - Object { - "stats.api.count": 2, - "stats.api.count2": 2, - "stats.total": 2, - } - `); - expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + const now = new Date().getTime(); + // Initialize counters + await repository.incrementCounter( + 'test_counter_type', + 'counter_4', + ['stats.existing_field', 'stats.new_field'], + { initialize: true } + ); + const result = await repository.get('test_counter_type', 'counter_4'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.existing_field": 1, + "stats.new_field": 0, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 9fb462e6f489a1..b477be947f1066 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -101,6 +101,8 @@ export interface SavedObjectsRepositoryOptions { * @public */ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { + /** Sets all the counter fields to 0 if they don't already exist */ + initialize?: boolean; migrationVersion?: SavedObjectsMigrationVersion; /** The Elasticsearch Refresh setting for this operation */ refresh?: MutatingOperationRefreshSetting; @@ -1576,7 +1578,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING } = options; + const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING, initialize = false } = options; const namespace = normalizeNamespace(options.namespace); const time = this._getCurrentTime(); @@ -1595,7 +1597,7 @@ export class SavedObjectsRepository { ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), attributes: counterFieldNames.reduce((acc, counterFieldName) => { - acc[counterFieldName] = 1; + acc[counterFieldName] = initialize ? 0 : 1; return acc; }, {} as Record), migrationVersion, @@ -1624,7 +1626,7 @@ export class SavedObjectsRepository { `, lang: 'painless', params: { - count: 1, + count: initialize ? 0 : 1, time, type, counterFieldNames, From c196193b9de86b433aa41b073afc8801aa3fd950 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 30 Nov 2020 17:01:37 +0100 Subject: [PATCH 6/8] Move usage collection-specific docs out of repository into usage collection plugins readme --- .../saved_objects/service/lib/repository.ts | 25 ++--- src/plugins/usage_collection/README.md | 93 ++++++++++++++++++- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index b477be947f1066..6d4576bfce14d4 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1521,35 +1521,24 @@ export class SavedObjectsRepository { * if one doesn't exist for the given id. * * @remarks - * When using incrementCounter for collecting usage data, you need to ensure - * that usage collection happens on a best-effort basis and doesn't - * negatively affect your plugin or users (see the example): - * - Swallow any exceptions thrown from the incrementCounter method and log - * a message in development. - * - Don't block your application on the incrementCounter method (e.g. - * don't use `await`) - * @remarks - * When supplying a field name like `stats.api.counter` the field name will + * * When supplying a field name like `stats.api.counter` the field name will * be used as-is to create a document like: * `{attributes: {'stats.api.counter': 1}}` * It will not create a nested structure like: * `{attributes: {stats: {api: {counter: 1}}}}` + * When using incrementCounter for collecting usage data, you need to ensure + * that usage collection happens on a best-effort basis and doesn't + * negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.md#tracking-interactions-with-incrementcounter) * * @example - * Collecting usage data * ```ts * const repository = coreStart.savedObjects.createInternalRepository(); * - * // NOTE: Usage collection happens on a best-effort basis, so we don't - * // `await` the promise returned by `incrementCounter` and we swallow any - * // exceptions in production. * repository - * .incrementCounter('test_counter_type', 'counter_2', [ - * 'stats.api.count', - * 'stats.api.count2', - * 'stats.total', + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * 'stats.apiCalls', + * 'stats.sampleDataInstalled', * ]) - * .catch((e) => (coreContext.env.cliArgs.dev ? logger.error(e) : e)); * ``` * * @param {string} type diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 5e6ed901c76474..33f7993f142338 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -140,6 +140,98 @@ export function registerMyPluginUsageCollector( } ``` +## Tracking interactions with incrementCounter +There are several ways to collect data that can provide insight into how users +use your plugin or specific features. For tracking user interactions the +`SavedObjectsRepository` provided by Core provides a useful `incrementCounter` +method which can be used to increment one or more counter fields in a +document. Examples of interactions include tracking: + - the number of API calls + - the number of times users installed and uninstalled the sample datasets + +When using `incrementCounter` for collecting usage data, you need to ensure +that usage collection happens on a best-effort basis and doesn't +negatively affect your plugin or users (see the example): + - Swallow any exceptions thrown from the incrementCounter method and log + a message in development. + - Don't block your application on the incrementCounter method (e.g. + don't use `await`) + - Set the `refresh` option to false to prevent unecessary index refreshes + which slows down Elasticsearch performance + + +Note: for brevity the following example does not follow Kibana's conventions +for structuring your plugin code. +```ts +// src/plugins/dashboard/server/plugin.ts + +import { PluginInitializerContext, Plugin, CoreStart, CoreSetup } from '../../src/core/server'; + +export class DashboardPlugin implements Plugin { + private readonly logger: Logger; + private readonly isDevEnvironment: boolean; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.isDevEnvironment = initializerContext.env.cliArgs.dev; + } + public setup(core) { + // Register a saved object type to store our usage counters + core.savedObjects.registerType({ + // Don't expose this saved object type via the saved objects HTTP API + hidden: true, + mappings: { + // Since we're not querying or aggregating over our counter documents + // we don't define any fields. + dynamic: false, + properties: {}, + }, + name: 'dashboard_usage_counters', + namespaceType: 'single', + }); + } + public start(core) { + const repository = core.savedObjects.createInternalRepository(['dashboard_usage_counters']); + // Initialize all the counter fields to 0 when our plugin starts + // NOTE: Usage collection happens on a best-effort basis, so we don't + // `await` the promise returned by `incrementCounter` and we swallow any + // exceptions in production. + repository + .incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [ + 'apiCalls', + 'settingToggled', + ], {refresh: false, initialize: true}) + .catch((e) => (this.isDevEnvironment ? this.logger.error(e) : e)); + + const router = core.http.createRouter(); + + router.post( + { + path: `api/v1/dashboard/counters/{counter}`, + validate: { + params: schema.object({ + counter: schema.oneOf([schema.literal('apiCalls'), schema.literal('settingToggled')]), + }), + }, + }, + async (context, request, response) => { + request.params.id + + // NOTE: Usage collection happens on a best-effort basis, so we don't + // `await` the promise returned by `incrementCounter` and we swallow any + // exceptions in production. + repository + .incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [ + counter + ], {refresh: false}) + .catch((e) => (this.isDevEnvironement ? this.logger.error(e) : e)); + + return response.ok(); + } + ); + } +} + ## Schema Field The `schema` field is a proscribed data model assists with detecting changes in usage collector payloads. To define the collector schema add a schema field that specifies every possible field reported when registering the collector. Whenever the `schema` field is set or changed please run `node scripts/telemetry_check.js --fix` to update the stored schema json files. @@ -200,7 +292,6 @@ export const myCollector = makeUsageCollector({ }, }); ``` - ## Update the telemetry payload and telemetry cluster field mappings There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. From d268882923ecf0ed2f3a6658e9bf8cdf8bff2010 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 30 Nov 2020 20:56:09 +0100 Subject: [PATCH 7/8] Update api docs --- ...vedobjectsincrementcounteroptions.initialize.md | 13 +++++++++++++ ...e-server.savedobjectsincrementcounteroptions.md | 1 + ...rver.savedobjectsrepository.incrementcounter.md | 14 ++++---------- src/core/server/server.api.md | 1 + 4 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md new file mode 100644 index 00000000000000..2e1e409e2d7f90 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) > [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) + +## SavedObjectsIncrementCounterOptions.initialize property + +Sets all the counter fields to 0 if they don't already exist + +Signature: + +```typescript +initialize?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md index 6077945ddd376c..a885982194fa6a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md @@ -15,6 +15,7 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt | Property | Type | Description | | --- | --- | --- | +| [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | Sets all the counter fields to 0 if they don't already exist | | [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | | | [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index b657ee7343c5e2..4022467832660e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -29,25 +29,19 @@ incrementCounter(type: string, id: string, counterFieldNames: string[], options? ## Remarks -When supplying a field name like `stats.api.counter` the field name will be used as-is to create a document like: `{attributes: {'stats.api.counter': 1}}` It will not create a nested structure like: `{attributes: {stats: {api: {counter: 1}}}}` +\* When supplying a field name like `stats.api.counter` the field name will be used as-is to create a document like: `{attributes: {'stats.api.counter': 1}}` It will not create a nested structure like: `{attributes: {stats: {api: {counter: 1}}}}` When using incrementCounter for collecting usage data, you need to ensure that usage collection happens on a best-effort basis and doesn't negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage\_collection/README.md\#tracking-interactions-with-incrementcounter) ## Example -Collecting usage data ```ts const repository = coreStart.savedObjects.createInternalRepository(); -// NOTE: Usage collection happens on a best-effort basis, so we don't -// `await` the promise returned by `incrementCounter` and we swallow any -// exceptions in production. repository - .incrementCounter('test_counter_type', 'counter_2', [ - 'stats.api.count', - 'stats.api.count2', - 'stats.total', + .incrementCounter('dashboard_counter_type', 'counter_id', [ + 'stats.apiCalls', + 'stats.sampleDataInstalled', ]) - .catch((e) => (coreContext.env.cliArgs.dev ? logger.error(e) : e)); ``` diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ce04c1b5412d1d..ad31b1ffc682c7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2366,6 +2366,7 @@ export interface SavedObjectsImportUnsupportedTypeError { // @public (undocumented) export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { + initialize?: boolean; // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; refresh?: MutatingOperationRefreshSetting; From 308a09848f57f529978efbc13d3a774784952541 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 30 Nov 2020 22:07:08 +0100 Subject: [PATCH 8/8] Polish generated docs --- ...jectsincrementcounteroptions.initialize.md | 2 +- ...ver.savedobjectsincrementcounteroptions.md | 6 ++-- ...ncrementcounteroptions.migrationversion.md | 2 ++ ...dobjectsincrementcounteroptions.refresh.md | 2 +- ...savedobjectsrepository.incrementcounter.md | 21 ++++++++---- .../saved_objects/service/lib/repository.ts | 32 +++++++++++++------ 6 files changed, 45 insertions(+), 20 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md index 2e1e409e2d7f90..61091306d0dbc8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md @@ -4,7 +4,7 @@ ## SavedObjectsIncrementCounterOptions.initialize property -Sets all the counter fields to 0 if they don't already exist +(default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md index a885982194fa6a..68e9bb09456cd5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md @@ -15,7 +15,7 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt | Property | Type | Description | | --- | --- | --- | -| [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | Sets all the counter fields to 0 if they don't already exist | -| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | | -| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. | +| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | +| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md index 417db99fd5a27a..aff80138d61cf3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObjectsIncrementCounterOptions.migrationVersion property +[SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md index 31d957ca30a3e4..4f217cc223d46e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md @@ -4,7 +4,7 @@ ## SavedObjectsIncrementCounterOptions.refresh property -The Elasticsearch Refresh setting for this operation +(default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index 4022467832660e..dc62cacf6741bc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -16,20 +16,22 @@ incrementCounter(type: string, id: string, counterFieldNames: string[], options? | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| counterFieldNames | string[] | | -| options | SavedObjectsIncrementCounterOptions | | +| type | string | The type of saved object whose fields should be incremented | +| id | string | The id of the document whose fields should be incremented | +| counterFieldNames | string[] | An array of field names to increment | +| options | SavedObjectsIncrementCounterOptions | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | Returns: `Promise` -{promise} +The saved object after the specified fields were incremented ## Remarks -\* When supplying a field name like `stats.api.counter` the field name will be used as-is to create a document like: `{attributes: {'stats.api.counter': 1}}` It will not create a nested structure like: `{attributes: {stats: {api: {counter: 1}}}}` When using incrementCounter for collecting usage data, you need to ensure that usage collection happens on a best-effort basis and doesn't negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage\_collection/README.md\#tracking-interactions-with-incrementcounter) +When supplying a field name like `stats.api.counter` the field name will be used as-is to create a document like: `{attributes: {'stats.api.counter': 1}}` It will not create a nested structure like: `{attributes: {stats: {api: {counter: 1}}}}` + +When using incrementCounter for collecting usage data, you need to ensure that usage collection happens on a best-effort basis and doesn't negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage\_collection/README.md\#tracking-interactions-with-incrementcounter) ## Example @@ -37,10 +39,17 @@ incrementCounter(type: string, id: string, counterFieldNames: string[], options? ```ts const repository = coreStart.savedObjects.createInternalRepository(); +// Initialize all fields to 0 repository .incrementCounter('dashboard_counter_type', 'counter_id', [ 'stats.apiCalls', 'stats.sampleDataInstalled', + ], {initialize: true}); + +// Increment the apiCalls field counter +repository + .incrementCounter('dashboard_counter_type', 'counter_id', [ + 'stats.apiCalls', ]) ``` diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 6d4576bfce14d4..2f09ad71de5587 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -101,10 +101,17 @@ export interface SavedObjectsRepositoryOptions { * @public */ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { - /** Sets all the counter fields to 0 if they don't already exist */ + /** + * (default=false) If true, sets all the counter fields to 0 if they don't + * already exist. Existing fields will be left as-is and won't be incremented. + */ initialize?: boolean; + /** {@link SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; - /** The Elasticsearch Refresh setting for this operation */ + /** + * (default='wait_for') The Elasticsearch refresh setting for this + * operation. See {@link MutatingOperationRefreshSetting} + */ refresh?: MutatingOperationRefreshSetting; } @@ -1521,11 +1528,12 @@ export class SavedObjectsRepository { * if one doesn't exist for the given id. * * @remarks - * * When supplying a field name like `stats.api.counter` the field name will + * When supplying a field name like `stats.api.counter` the field name will * be used as-is to create a document like: * `{attributes: {'stats.api.counter': 1}}` * It will not create a nested structure like: * `{attributes: {stats: {api: {counter: 1}}}}` + * * When using incrementCounter for collecting usage data, you need to ensure * that usage collection happens on a best-effort basis and doesn't * negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.md#tracking-interactions-with-incrementcounter) @@ -1534,19 +1542,25 @@ export class SavedObjectsRepository { * ```ts * const repository = coreStart.savedObjects.createInternalRepository(); * + * // Initialize all fields to 0 * repository * .incrementCounter('dashboard_counter_type', 'counter_id', [ * 'stats.apiCalls', * 'stats.sampleDataInstalled', + * ], {initialize: true}); + * + * // Increment the apiCalls field counter + * repository + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * 'stats.apiCalls', * ]) * ``` * - * @param {string} type - * @param {string} id - * @param {string} counterFieldNames - * @param {object} [options={}] - * @property {object} [options.migrationVersion=undefined] - * @returns {promise} + * @param type - The type of saved object whose fields should be incremented + * @param id - The id of the document whose fields should be incremented + * @param counterFieldNames - An array of field names to increment + * @param options - {@link SavedObjectsIncrementCounterOptions} + * @returns The saved object after the specified fields were incremented */ async incrementCounter( type: string,