Skip to content

Commit

Permalink
Merge branch 'master' into move-index-patterns-server-pt2
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticmachine authored Nov 5, 2019
2 parents b1ad415 + 5ba237a commit 52b18c2
Show file tree
Hide file tree
Showing 25 changed files with 468 additions and 70 deletions.
18 changes: 16 additions & 2 deletions src/legacy/core_plugins/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ const telemetry = (kibana: any) => {
// `config` is used internally and not intended to be set
config: Joi.string().default(Joi.ref('$defaultConfigPath')),
banner: Joi.boolean().default(true),
lastVersionChecked: Joi.string()
.allow('')
.default(''),
url: Joi.when('$dev', {
is: true,
then: Joi.string().default(
Expand Down Expand Up @@ -77,7 +80,8 @@ const telemetry = (kibana: any) => {
},
},
async replaceInjectedVars(originalInjectedVars: any, request: any) {
const telemetryOptedIn = await getTelemetryOptIn(request);
const currentKibanaVersion = getCurrentKibanaVersion(request.server);
const telemetryOptedIn = await getTelemetryOptIn({ request, currentKibanaVersion });

return {
...originalInjectedVars,
Expand All @@ -97,7 +101,13 @@ const telemetry = (kibana: any) => {
mappings,
},
init(server: Server) {
const initializerContext = {} as PluginInitializerContext;
const initializerContext = {
env: {
packageInfo: {
version: getCurrentKibanaVersion(server),
},
},
} as PluginInitializerContext;

const coreSetup = ({
http: { server },
Expand All @@ -116,3 +126,7 @@ const telemetry = (kibana: any) => {

// eslint-disable-next-line import/no-default-export
export default telemetry;

function getCurrentKibanaVersion(server: Server): string {
return server.config().get('pkg.version');
}
3 changes: 3 additions & 0 deletions src/legacy/core_plugins/telemetry/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"properties": {
"enabled": {
"type": "boolean"
},
"lastVersionChecked": {
"type": "keyword"
}
}
}
Expand Down
214 changes: 214 additions & 0 deletions src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* 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 { getTelemetryOptIn } from './get_telemetry_opt_in';

describe('get_telemetry_opt_in', () => {
it('returns false when request path is not /app*', async () => {
const params = getCallGetTelemetryOptInParams({
requestPath: '/foo/bar',
});

const result = await callGetTelemetryOptIn(params);

expect(result).toBe(false);
});

it('returns null when saved object not found', async () => {
const params = getCallGetTelemetryOptInParams({
savedObjectNotFound: true,
});

const result = await callGetTelemetryOptIn(params);

expect(result).toBe(null);
});

it('returns false when saved object forbidden', async () => {
const params = getCallGetTelemetryOptInParams({
savedObjectForbidden: true,
});

const result = await callGetTelemetryOptIn(params);

expect(result).toBe(false);
});

it('throws an error on unexpected saved object error', async () => {
const params = getCallGetTelemetryOptInParams({
savedObjectOtherError: true,
});

let threw = false;
try {
await callGetTelemetryOptIn(params);
} catch (err) {
threw = true;
expect(err.message).toBe(SavedObjectOtherErrorMessage);
}

expect(threw).toBe(true);
});

it('returns null if enabled is null or undefined', async () => {
for (const enabled of [null, undefined]) {
const params = getCallGetTelemetryOptInParams({
enabled,
});

const result = await callGetTelemetryOptIn(params);

expect(result).toBe(null);
}
});

it('returns true when enabled is true', async () => {
const params = getCallGetTelemetryOptInParams({
enabled: true,
});

const result = await callGetTelemetryOptIn(params);

expect(result).toBe(true);
});

// build a table of tests with version checks, with results for enabled false
type VersionCheckTable = Array<Partial<CallGetTelemetryOptInParams>>;

const EnabledFalseVersionChecks: VersionCheckTable = [
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '8.0.0', result: false },
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '8.0.1', result: false },
{ lastVersionChecked: '8.0.1', currentKibanaVersion: '8.0.0', result: false },
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '8.1.0', result: null },
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '9.0.0', result: null },
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '7.0.0', result: false },
{ lastVersionChecked: '8.1.0', currentKibanaVersion: '8.0.0', result: false },
{ lastVersionChecked: '8.0.0-X', currentKibanaVersion: '8.0.0', result: false },
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '8.0.0-X', result: false },
{ lastVersionChecked: null, currentKibanaVersion: '8.0.0', result: null },
{ lastVersionChecked: undefined, currentKibanaVersion: '8.0.0', result: null },
{ lastVersionChecked: 5, currentKibanaVersion: '8.0.0', result: null },
{ lastVersionChecked: '8.0.0', currentKibanaVersion: 'beta', result: null },
{ lastVersionChecked: 'beta', currentKibanaVersion: '8.0.0', result: null },
{ lastVersionChecked: 'beta', currentKibanaVersion: 'beta', result: false },
{ lastVersionChecked: 'BETA', currentKibanaVersion: 'beta', result: null },
].map(el => ({ ...el, enabled: false }));

// build a table of tests with version checks, with results for enabled true/null/undefined
const EnabledTrueVersionChecks: VersionCheckTable = EnabledFalseVersionChecks.map(el => ({
...el,
enabled: true,
result: true,
}));

const EnabledNullVersionChecks: VersionCheckTable = EnabledFalseVersionChecks.map(el => ({
...el,
enabled: null,
result: null,
}));

const EnabledUndefinedVersionChecks: VersionCheckTable = EnabledFalseVersionChecks.map(el => ({
...el,
enabled: undefined,
result: null,
}));

const AllVersionChecks = [
...EnabledFalseVersionChecks,
...EnabledTrueVersionChecks,
...EnabledNullVersionChecks,
...EnabledUndefinedVersionChecks,
];

test.each(AllVersionChecks)(
'returns expected result for version check with %j',
async (params: Partial<CallGetTelemetryOptInParams>) => {
const result = await callGetTelemetryOptIn({ ...DefaultParams, ...params });
expect(result).toBe(params.result);
}
);
});

interface CallGetTelemetryOptInParams {
requestPath: string;
savedObjectNotFound: boolean;
savedObjectForbidden: boolean;
savedObjectOtherError: boolean;
enabled: boolean | null | undefined;
lastVersionChecked?: any; // should be a string, but test with non-strings
currentKibanaVersion: string;
result?: boolean | null;
}

const DefaultParams = {
requestPath: '/app/something',
savedObjectNotFound: false,
savedObjectForbidden: false,
savedObjectOtherError: false,
enabled: true,
lastVersionChecked: '8.0.0',
currentKibanaVersion: '8.0.0',
};

function getCallGetTelemetryOptInParams(
overrides: Partial<CallGetTelemetryOptInParams>
): CallGetTelemetryOptInParams {
return { ...DefaultParams, ...overrides };
}

async function callGetTelemetryOptIn(params: CallGetTelemetryOptInParams): Promise<boolean | null> {
const { currentKibanaVersion } = params;
const request = getMockRequest(params);
return await getTelemetryOptIn({ request, currentKibanaVersion });
}

function getMockRequest(params: CallGetTelemetryOptInParams): any {
return {
path: params.requestPath,
getSavedObjectsClient() {
return getMockSavedObjectsClient(params);
},
};
}

const SavedObjectNotFoundMessage = 'savedObjectNotFound';
const SavedObjectForbiddenMessage = 'savedObjectForbidden';
const SavedObjectOtherErrorMessage = 'savedObjectOtherError';

function getMockSavedObjectsClient(params: CallGetTelemetryOptInParams) {
return {
async get(type: string, id: string) {
if (params.savedObjectNotFound) throw new Error(SavedObjectNotFoundMessage);
if (params.savedObjectForbidden) throw new Error(SavedObjectForbiddenMessage);
if (params.savedObjectOtherError) throw new Error(SavedObjectOtherErrorMessage);

const enabled = params.enabled;
const lastVersionChecked = params.lastVersionChecked;
return { attributes: { enabled, lastVersionChecked } };
},
errors: {
isNotFoundError(error: any) {
return error.message === SavedObjectNotFoundMessage;
},
isForbiddenError(error: any) {
return error.message === SavedObjectForbiddenMessage;
},
},
};
}
66 changes: 63 additions & 3 deletions src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,21 @@
* under the License.
*/

export async function getTelemetryOptIn(request: any) {
import semver from 'semver';

import { SavedObjectAttributes } from './routes/opt_in';

interface GetTelemetryOptIn {
request: any;
currentKibanaVersion: string;
}

// Returns whether telemetry has been opt'ed into or not.
// Returns null not set, meaning Kibana should prompt in the UI.
export async function getTelemetryOptIn({
request,
currentKibanaVersion,
}: GetTelemetryOptIn): Promise<boolean | null> {
const isRequestingApplication = request.path.startsWith('/app');

// Prevent interstitial screens (such as the space selector) from prompting for telemetry
Expand All @@ -27,9 +41,9 @@ export async function getTelemetryOptIn(request: any) {

const savedObjectsClient = request.getSavedObjectsClient();

let savedObject;
try {
const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry');
return attributes.enabled;
savedObject = await savedObjectsClient.get('telemetry', 'telemetry');
} catch (error) {
if (savedObjectsClient.errors.isNotFoundError(error)) {
return null;
Expand All @@ -43,4 +57,50 @@ export async function getTelemetryOptIn(request: any) {

throw error;
}

const { attributes }: { attributes: SavedObjectAttributes } = savedObject;

// if enabled is already null, return null
if (attributes.enabled == null) return null;

const enabled = !!attributes.enabled;

// if enabled is true, return it
if (enabled === true) return enabled;

// Additional check if they've already opted out (enabled: false):
// - if the Kibana version has changed by at least a minor version,
// return null to re-prompt.

const lastKibanaVersion = attributes.lastVersionChecked;

// if the last kibana version isn't set, or is somehow not a string, return null
if (typeof lastKibanaVersion !== 'string') return null;

// if version hasn't changed, just return enabled value
if (lastKibanaVersion === currentKibanaVersion) return enabled;

const lastSemver = parseSemver(lastKibanaVersion);
const currentSemver = parseSemver(currentKibanaVersion);

// if either version is invalid, return null
if (lastSemver == null || currentSemver == null) return null;

// actual major/minor version comparison, for cases when to return null
if (currentSemver.major > lastSemver.major) return null;
if (currentSemver.major === lastSemver.major) {
if (currentSemver.minor > lastSemver.minor) return null;
}

// current version X.Y is not greater than last version X.Y, return enabled
return enabled;
}

function parseSemver(version: string): semver.SemVer | null {
// semver functions both return nulls AND throw exceptions: "it depends!"
try {
return semver.parse(version);
} catch (err) {
return null;
}
}
2 changes: 1 addition & 1 deletion src/legacy/core_plugins/telemetry/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ export { getTelemetryOptIn } from './get_telemetry_opt_in';
export { telemetryCollectionManager } from './collection_manager';

export const telemetryPlugin = (initializerContext: PluginInitializerContext) =>
new TelemetryPlugin();
new TelemetryPlugin(initializerContext);
export { constants };
11 changes: 9 additions & 2 deletions src/legacy/core_plugins/telemetry/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
* under the License.
*/

import { CoreSetup } from 'src/core/server';
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { registerRoutes } from './routes';
import { telemetryCollectionManager } from './collection_manager';
import { getStats } from './telemetry_collection';

export class TelemetryPlugin {
private readonly currentKibanaVersion: string;

constructor(initializerContext: PluginInitializerContext) {
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
}

public setup(core: CoreSetup) {
const currentKibanaVersion = this.currentKibanaVersion;
telemetryCollectionManager.setStatsGetter(getStats, 'local');
registerRoutes(core);
registerRoutes({ core, currentKibanaVersion });
}
}
9 changes: 7 additions & 2 deletions src/legacy/core_plugins/telemetry/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import { CoreSetup } from 'src/core/server';
import { registerOptInRoutes } from './opt_in';
import { registerTelemetryDataRoutes } from './telemetry_stats';

export function registerRoutes(core: CoreSetup) {
registerOptInRoutes(core);
interface RegisterRoutesParams {
core: CoreSetup;
currentKibanaVersion: string;
}

export function registerRoutes({ core, currentKibanaVersion }: RegisterRoutesParams) {
registerOptInRoutes({ core, currentKibanaVersion });
registerTelemetryDataRoutes(core);
}
Loading

0 comments on commit 52b18c2

Please sign in to comment.