diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4edca2f43e6a4..a1e3bdcb2d198 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,6 +28,7 @@ packages/kbn-ambient-storybook-types @elastic/kibana-operations packages/kbn-ambient-ui-types @elastic/kibana-operations packages/kbn-analytics @elastic/kibana-core packages/analytics/client @elastic/kibana-core +packages/analytics/utils/analytics_collection_utils @elastic/kibana-core test/analytics/plugins/analytics_ftr_helpers @elastic/kibana-core test/analytics/plugins/analytics_plugin_a @elastic/kibana-core packages/analytics/shippers/elastic_v3/browser @elastic/kibana-core diff --git a/package.json b/package.json index 495fe098ada51..37c915ba3d20c 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "@kbn/alerts-ui-shared": "link:packages/kbn-alerts-ui-shared", "@kbn/analytics": "link:packages/kbn-analytics", "@kbn/analytics-client": "link:packages/analytics/client", + "@kbn/analytics-collection-utils": "link:packages/analytics/utils/analytics_collection_utils", "@kbn/analytics-ftr-helpers-plugin": "link:test/analytics/plugins/analytics_ftr_helpers", "@kbn/analytics-plugin-a-plugin": "link:test/analytics/plugins/analytics_plugin_a", "@kbn/analytics-shippers-elastic-v3-browser": "link:packages/analytics/shippers/elastic_v3/browser", diff --git a/packages/analytics/utils/analytics_collection_utils/README.md b/packages/analytics/utils/analytics_collection_utils/README.md new file mode 100644 index 0000000000000..fd01a04f534b7 --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/README.md @@ -0,0 +1,3 @@ +# @kbn/analytics-collection-utils + +Empty package generated by @kbn/generate diff --git a/packages/analytics/utils/analytics_collection_utils/index.ts b/packages/analytics/utils/analytics_collection_utils/index.ts new file mode 100644 index 0000000000000..4ead4333b67cc --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { isSyntheticsMonitor } from './src'; diff --git a/packages/analytics/utils/analytics_collection_utils/jest.config.js b/packages/analytics/utils/analytics_collection_utils/jest.config.js new file mode 100644 index 0000000000000..93724717599fa --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/analytics/utils/analytics_collection_utils'], +}; diff --git a/packages/analytics/utils/analytics_collection_utils/kibana.jsonc b/packages/analytics/utils/analytics_collection_utils/kibana.jsonc new file mode 100644 index 0000000000000..f909070bbe36f --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/analytics-collection-utils", + "owner": "@elastic/kibana-core" +} diff --git a/packages/analytics/utils/analytics_collection_utils/package.json b/packages/analytics/utils/analytics_collection_utils/package.json new file mode 100644 index 0000000000000..eb10b67ce94dd --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/analytics-collection-utils", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/analytics/utils/analytics_collection_utils/src/index.ts b/packages/analytics/utils/analytics_collection_utils/src/index.ts new file mode 100644 index 0000000000000..48d4cd08c568d --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { isSyntheticsMonitor } from './is_synthetics_monitor'; diff --git a/packages/analytics/utils/analytics_collection_utils/src/is_synthetics_monitor.test.ts b/packages/analytics/utils/analytics_collection_utils/src/is_synthetics_monitor.test.ts new file mode 100644 index 0000000000000..02da2a7e41606 --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/src/is_synthetics_monitor.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isSyntheticsMonitor } from './is_synthetics_monitor'; + +describe('isSyntheticsMonitor', () => { + test('returns false for any user agent', () => { + expect(isSyntheticsMonitor()).toBe(false); + }); + + test('returns true for when the user agent contains "Elastic/Synthetics"', () => { + jest + .spyOn(window.navigator, 'userAgent', 'get') + .mockReturnValue(window.navigator.userAgent + 'Elastic/Synthetics'); + expect(isSyntheticsMonitor()).toBe(true); + }); +}); diff --git a/packages/analytics/utils/analytics_collection_utils/src/is_synthetics_monitor.ts b/packages/analytics/utils/analytics_collection_utils/src/is_synthetics_monitor.ts new file mode 100644 index 0000000000000..093a984f32d5d --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/src/is_synthetics_monitor.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Returns whether the current navigation is performed by a Synthetics monitor + * (and hence, telemetry should not be collected). + */ +export function isSyntheticsMonitor(): boolean { + return window.navigator.userAgent.includes('Elastic/Synthetics'); +} diff --git a/packages/analytics/utils/analytics_collection_utils/tsconfig.json b/packages/analytics/utils/analytics_collection_utils/tsconfig.json new file mode 100644 index 0000000000000..b05325b824a67 --- /dev/null +++ b/packages/analytics/utils/analytics_collection_utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/src/plugins/telemetry/public/plugin.test.mock.ts b/src/plugins/telemetry/public/plugin.test.mock.ts new file mode 100644 index 0000000000000..85af98e239a0b --- /dev/null +++ b/src/plugins/telemetry/public/plugin.test.mock.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const isSyntheticsMonitorMock = jest.fn(); +jest.doMock('@kbn/analytics-collection-utils', () => ({ + isSyntheticsMonitor: isSyntheticsMonitorMock, +})); diff --git a/src/plugins/telemetry/public/plugin.test.ts b/src/plugins/telemetry/public/plugin.test.ts index 13ebb4d999315..842e11f12cc68 100644 --- a/src/plugins/telemetry/public/plugin.test.ts +++ b/src/plugins/telemetry/public/plugin.test.ts @@ -6,12 +6,14 @@ * Side Public License, v 1. */ +import { of } from 'rxjs'; import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; import { coreMock } from '@kbn/core/public/mocks'; import { homePluginMock } from '@kbn/home-plugin/public/mocks'; import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; +import { isSyntheticsMonitorMock } from './plugin.test.mock'; import { TelemetryPlugin } from './plugin'; describe('TelemetryPlugin', () => { @@ -21,6 +23,7 @@ describe('TelemetryPlugin', () => { beforeEach(() => { screenshotMode = screenshotModePluginMock.createSetupContract(); home = homePluginMock.createSetupContract(); + isSyntheticsMonitorMock.mockReturnValue(false); }); describe('setup', () => { @@ -82,5 +85,44 @@ describe('TelemetryPlugin', () => { ); }); }); + + it('disables telemetry when in screenshot mode', async () => { + const initializerContext = coreMock.createPluginInitializerContext(); + + const plugin = new TelemetryPlugin(initializerContext); + const isScreenshotModeSpy = jest + .spyOn(screenshotMode, 'isScreenshotMode') + .mockReturnValue(true); + plugin.setup(coreMock.createSetup(), { screenshotMode, home }); + expect(isScreenshotModeSpy).toBeCalledTimes(1); + + const coreStartMock = coreMock.createStart(); + coreStartMock.application = { ...coreStartMock.application, currentAppId$: of('some-app') }; + const optInSpy = jest.spyOn(coreStartMock.analytics, 'optIn'); + plugin.start(coreStartMock, { screenshotMode }); + expect(isScreenshotModeSpy).toBeCalledTimes(2); + expect(optInSpy).toBeCalledTimes(1); + expect(optInSpy).toHaveBeenCalledWith({ global: { enabled: false } }); + }); + + it('disables telemetry when the user agent contains Elastic/Synthetics', async () => { + const initializerContext = coreMock.createPluginInitializerContext(); + + const plugin = new TelemetryPlugin(initializerContext); + const isScreenshotModeSpy = jest + .spyOn(screenshotMode, 'isScreenshotMode') + .mockReturnValue(false); + plugin.setup(coreMock.createSetup(), { screenshotMode, home }); + expect(isScreenshotModeSpy).toBeCalledTimes(1); + + const coreStartMock = coreMock.createStart(); + coreStartMock.application = { ...coreStartMock.application, currentAppId$: of('some-app') }; + isSyntheticsMonitorMock.mockReturnValueOnce(true); + const optInSpy = jest.spyOn(coreStartMock.analytics, 'optIn'); + plugin.start(coreStartMock, { screenshotMode }); + expect(isScreenshotModeSpy).toBeCalledTimes(2); + expect(optInSpy).toBeCalledTimes(1); + expect(optInSpy).toHaveBeenCalledWith({ global: { enabled: false } }); + }); }); }); diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 59ff2d3478c1d..588a532ffa6a3 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -23,6 +23,7 @@ import type { } from '@kbn/screenshot-mode-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; +import { isSyntheticsMonitor } from '@kbn/analytics-collection-utils'; import { BehaviorSubject, map, switchMap, tap } from 'rxjs'; import type { TelemetryConfigLabels } from '../server/config'; @@ -159,7 +160,7 @@ export class TelemetryPlugin const currentKibanaVersion = this.currentKibanaVersion; this.telemetryService = new TelemetryService({ config, - isScreenshotMode: screenshotMode.isScreenshotMode(), + isScreenshotMode: this.shouldSkipTelemetry(screenshotMode), http, notifications, currentKibanaVersion, @@ -200,7 +201,9 @@ export class TelemetryPlugin this.telemetrySender = new TelemetrySender(this.telemetryService, async () => { await this.refreshConfig(http); analytics.optIn({ - global: { enabled: this.telemetryService!.isOptedIn && !screenshotMode.isScreenshotMode() }, + global: { + enabled: this.telemetryService!.isOptedIn && !this.shouldSkipTelemetry(screenshotMode), + }, }); }); @@ -249,12 +252,18 @@ export class TelemetryPlugin application.currentAppId$ .pipe( switchMap(async () => { + // Disable telemetry and terminate early if Kibana is running in a special "skip" mode + if (this.shouldSkipTelemetry(screenshotMode)) { + analytics.optIn({ global: { enabled: false } }); + return; + } + // Refresh and get telemetry config const updatedConfig = await this.refreshConfig(http); analytics.optIn({ global: { - enabled: this.telemetryService!.isOptedIn && !screenshotMode.isScreenshotMode(), + enabled: this.telemetryService!.isOptedIn, }, }); @@ -286,6 +295,16 @@ export class TelemetryPlugin this.telemetrySender?.stop(); } + /** + * Kibana should skip telemetry collection if reporting is taking a screenshot + * or Synthetics monitoring is navigating Kibana. + * @param screenshotMode {@link ScreenshotModePluginSetup} + * @private + */ + private shouldSkipTelemetry(screenshotMode: ScreenshotModePluginSetup): boolean { + return screenshotMode.isScreenshotMode() || isSyntheticsMonitor(); + } + private getTelemetryServicePublicApis(): TelemetryServicePublicApis { const telemetryService = this.telemetryService!; return { diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 638bfb4f722a7..4163f957837a7 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -35,6 +35,7 @@ "@kbn/core-http-browser-mocks", "@kbn/core-http-browser", "@kbn/core-http-server", + "@kbn/analytics-collection-utils", ], "exclude": [ "target/**/*", diff --git a/src/plugins/usage_collection/public/plugin.tsx b/src/plugins/usage_collection/public/plugin.tsx index b4193f44fb8e4..635e33f8fd0b0 100644 --- a/src/plugins/usage_collection/public/plugin.tsx +++ b/src/plugins/usage_collection/public/plugin.tsx @@ -18,6 +18,7 @@ import type { HttpSetup, } from '@kbn/core/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { isSyntheticsMonitor } from '@kbn/analytics-collection-utils'; import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import { createReporter, trackApplicationUsageChange } from './services'; import { ApplicationUsageContext } from './components/track_application_view'; @@ -150,7 +151,8 @@ export class UsageCollectionPlugin if ( this.config.uiCounters.enabled && !isUnauthenticated(http) && - !screenshotMode.isScreenshotMode() + !screenshotMode.isScreenshotMode() && + !isSyntheticsMonitor() ) { this.reporter.start(); this.applicationUsageTracker.start(); diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index db86bf99273df..9491794c07f60 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -21,6 +21,7 @@ "@kbn/utility-types", "@kbn/i18n", "@kbn/core-http-server-mocks", + "@kbn/analytics-collection-utils", ], "exclude": [ "target/**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index 7faad08e28f0d..f833b70622d32 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -50,6 +50,8 @@ "@kbn/analytics/*": ["packages/kbn-analytics/*"], "@kbn/analytics-client": ["packages/analytics/client"], "@kbn/analytics-client/*": ["packages/analytics/client/*"], + "@kbn/analytics-collection-utils": ["packages/analytics/utils/analytics_collection_utils"], + "@kbn/analytics-collection-utils/*": ["packages/analytics/utils/analytics_collection_utils/*"], "@kbn/analytics-ftr-helpers-plugin": ["test/analytics/plugins/analytics_ftr_helpers"], "@kbn/analytics-ftr-helpers-plugin/*": ["test/analytics/plugins/analytics_ftr_helpers/*"], "@kbn/analytics-plugin-a-plugin": ["test/analytics/plugins/analytics_plugin_a"], diff --git a/yarn.lock b/yarn.lock index d1c704d5622c1..2bde2182e3bd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3000,6 +3000,10 @@ version "0.0.0" uid "" +"@kbn/analytics-collection-utils@link:packages/analytics/utils/analytics_collection_utils": + version "0.0.0" + uid "" + "@kbn/analytics-ftr-helpers-plugin@link:test/analytics/plugins/analytics_ftr_helpers": version "0.0.0" uid ""