This PR adds two usage stats to our telemetry for KQL: * How many times people click the opt in/out toggle in the query bar UI * Which language Kibana admins have set as the global default in advanced settings
This commit is contained in:
parent
71e4600973
commit
d10f21ddea
|
@ -29,13 +29,14 @@ import { homeApi } from './server/routes/api/home';
|
|||
import { managementApi } from './server/routes/api/management';
|
||||
import { scriptsApi } from './server/routes/api/scripts';
|
||||
import { registerSuggestionsApi } from './server/routes/api/suggestions';
|
||||
import { registerKqlTelemetryApi } from './server/routes/api/kql_telemetry';
|
||||
import { registerFieldFormats } from './server/field_formats/register';
|
||||
import { registerTutorials } from './server/tutorials/register';
|
||||
import * as systemApi from './server/lib/system_api';
|
||||
import handleEsError from './server/lib/handle_es_error';
|
||||
import mappings from './mappings.json';
|
||||
import { getUiSettingDefaults } from './ui_setting_defaults';
|
||||
|
||||
import { makeKQLUsageCollector } from './server/lib/kql_usage_collector';
|
||||
import { injectVars } from './inject_vars';
|
||||
|
||||
const mkdirp = Promise.promisify(mkdirpNode);
|
||||
|
@ -119,6 +120,12 @@ export default function (kibana) {
|
|||
},
|
||||
],
|
||||
|
||||
savedObjectSchemas: {
|
||||
'kql-telemetry': {
|
||||
isNamespaceAgnostic: true,
|
||||
},
|
||||
},
|
||||
|
||||
injectDefaultVars(server, options) {
|
||||
return {
|
||||
kbnIndex: options.index,
|
||||
|
@ -156,8 +163,10 @@ export default function (kibana) {
|
|||
homeApi(server);
|
||||
managementApi(server);
|
||||
registerSuggestionsApi(server);
|
||||
registerKqlTelemetryApi(server);
|
||||
registerFieldFormats(server);
|
||||
registerTutorials(server);
|
||||
makeKQLUsageCollector(server);
|
||||
server.expose('systemApi', systemApi);
|
||||
server.expose('handleEsError', handleEsError);
|
||||
server.injectUiAppVars('kibana', () => injectVars(server));
|
||||
|
|
|
@ -167,5 +167,15 @@
|
|||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kql-telemetry": {
|
||||
"properties": {
|
||||
"optInCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"optOutCount": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { getUiSettingDefaults } from '../../../ui_setting_defaults';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const uiSettingDefaults = getUiSettingDefaults();
|
||||
const defaultSearchQueryLanguageSetting = uiSettingDefaults['search:queryLanguage'].value;
|
||||
|
||||
export async function fetch(callCluster) {
|
||||
const [response, config] = await Promise.all([
|
||||
callCluster('get', {
|
||||
index: '.kibana',
|
||||
type: 'doc',
|
||||
id: 'kql-telemetry:kql-telemetry',
|
||||
ignore: [404],
|
||||
}),
|
||||
callCluster('search', {
|
||||
index: '.kibana',
|
||||
body: { query: { term: { type: 'config' } } },
|
||||
ignore: [404],
|
||||
}),
|
||||
]);
|
||||
|
||||
const queryLanguageConfigValue = get(config, 'hits.hits[0]._source.config.search:queryLanguage');
|
||||
|
||||
// search:queryLanguage can potentially be in four states in the .kibana index:
|
||||
// 1. undefined: this means the user has never touched this setting
|
||||
// 2. null: this means the user has touched the setting, but the current value matches the default
|
||||
// 3. 'kuery' or 'lucene': this means the user has explicitly selected the given non-default language
|
||||
//
|
||||
// It's nice to know if the user has never touched the setting or if they tried kuery then
|
||||
// went back to the default, so I preserve this info by prefixing the language name with
|
||||
// 'default-' when the value in .kibana is undefined (case #1).
|
||||
let defaultLanguage;
|
||||
if (queryLanguageConfigValue === undefined) {
|
||||
defaultLanguage = `default-${defaultSearchQueryLanguageSetting}`;
|
||||
} else if (queryLanguageConfigValue === null) {
|
||||
defaultLanguage = defaultSearchQueryLanguageSetting;
|
||||
} else {
|
||||
defaultLanguage = queryLanguageConfigValue;
|
||||
}
|
||||
|
||||
const kqlTelemetryDoc = {
|
||||
optInCount: 0,
|
||||
optOutCount: 0,
|
||||
...get(response, '_source.kql-telemetry', {}),
|
||||
};
|
||||
|
||||
return {
|
||||
...kqlTelemetryDoc,
|
||||
defaultQueryLanguage: defaultLanguage,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('../../../ui_setting_defaults', () => ({
|
||||
getUiSettingDefaults: () => ({ 'search:queryLanguage': { value: 'lucene' } }),
|
||||
}));
|
||||
|
||||
import { fetch } from './fetch';
|
||||
|
||||
let callCluster;
|
||||
|
||||
function setupMockCallCluster(optCount, language) {
|
||||
callCluster = jest.fn((method, params) => {
|
||||
if ('id' in params && params.id === 'kql-telemetry:kql-telemetry') {
|
||||
if (optCount === null) {
|
||||
return Promise.resolve({
|
||||
_index: '.kibana_1',
|
||||
_type: 'doc',
|
||||
_id: 'kql-telemetry:kql-telemetry',
|
||||
found: false,
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
_source: {
|
||||
'kql-telemetry': {
|
||||
...optCount,
|
||||
},
|
||||
type: 'kql-telemetry',
|
||||
updated_at: '2018-10-05T20:20:56.258Z',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if ('body' in params && params.body.query.term.type === 'config') {
|
||||
if (language === 'missingConfigDoc') {
|
||||
Promise.resolve({
|
||||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
config: {
|
||||
'search:queryLanguage': language,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('makeKQLUsageCollector', () => {
|
||||
describe('fetch method', () => {
|
||||
it('should return opt in data from the .kibana/kql-telemetry doc', async () => {
|
||||
setupMockCallCluster({ optInCount: 1 }, 'kuery');
|
||||
const fetchResponse = await fetch(callCluster);
|
||||
expect(fetchResponse.optInCount).toBe(1);
|
||||
expect(fetchResponse.optOutCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should return the default query language set in advanced settings', async () => {
|
||||
setupMockCallCluster({ optInCount: 1 }, 'kuery');
|
||||
const fetchResponse = await fetch(callCluster);
|
||||
expect(fetchResponse.defaultQueryLanguage).toBe('kuery');
|
||||
});
|
||||
|
||||
// Indicates the user has modified the setting at some point but the value is currently the default
|
||||
it('should return the kibana default query language if the config value is null', async () => {
|
||||
setupMockCallCluster({ optInCount: 1 }, null);
|
||||
const fetchResponse = await fetch(callCluster);
|
||||
expect(fetchResponse.defaultQueryLanguage).toBe('lucene');
|
||||
});
|
||||
|
||||
it('should indicate when the default language has never been modified by the user', async () => {
|
||||
setupMockCallCluster({ optInCount: 1 }, undefined);
|
||||
const fetchResponse = await fetch(callCluster);
|
||||
expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene');
|
||||
});
|
||||
|
||||
it('should default to 0 opt in counts if the .kibana/kql-telemetry doc does not exist', async () => {
|
||||
setupMockCallCluster(null, 'kuery');
|
||||
const fetchResponse = await fetch(callCluster);
|
||||
expect(fetchResponse.optInCount).toBe(0);
|
||||
expect(fetchResponse.optOutCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should default to the kibana default language if the config document does not exist', async () => {
|
||||
setupMockCallCluster(null, 'missingConfigDoc');
|
||||
const fetchResponse = await fetch(callCluster);
|
||||
expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { makeKQLUsageCollector } from './make_kql_usage_collector';
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { fetch } from './fetch';
|
||||
|
||||
export function makeKQLUsageCollector(server) {
|
||||
const kqlUsageCollector = server.usage.collectorSet.makeUsageCollector({
|
||||
type: 'kql',
|
||||
fetch,
|
||||
});
|
||||
|
||||
server.usage.collectorSet.register(kqlUsageCollector);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { makeKQLUsageCollector } from './make_kql_usage_collector';
|
||||
|
||||
describe('makeKQLUsageCollector', () => {
|
||||
|
||||
let server;
|
||||
let makeUsageCollectorStub;
|
||||
let registerStub;
|
||||
|
||||
beforeEach(() => {
|
||||
makeUsageCollectorStub = jest.fn();
|
||||
registerStub = jest.fn();
|
||||
server = {
|
||||
usage: {
|
||||
collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should call collectorSet.register', () => {
|
||||
makeKQLUsageCollector(server);
|
||||
expect(registerStub).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call makeUsageCollector with type = kql', () => {
|
||||
makeKQLUsageCollector(server);
|
||||
expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1);
|
||||
expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('kql');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 Joi from 'joi';
|
||||
import Boom from 'boom';
|
||||
|
||||
export function registerKqlTelemetryApi(server) {
|
||||
server.route({
|
||||
path: '/api/kibana/kql_opt_in_telemetry',
|
||||
method: 'POST',
|
||||
config: {
|
||||
validate: {
|
||||
payload: {
|
||||
opt_in: Joi.bool().required(),
|
||||
},
|
||||
},
|
||||
tags: ['api'],
|
||||
},
|
||||
handler: async function (request, reply) {
|
||||
const { savedObjects: { getSavedObjectsRepository } } = server;
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
|
||||
const {
|
||||
payload: { opt_in: optIn },
|
||||
} = request;
|
||||
|
||||
const counterName = optIn ? 'optInCount' : 'optOutCount';
|
||||
|
||||
try {
|
||||
await internalRepository.incrementCounter(
|
||||
'kql-telemetry',
|
||||
'kql-telemetry',
|
||||
counterName,
|
||||
);
|
||||
}
|
||||
catch (error) {
|
||||
reply(new Boom('Something went wrong', { statusCode: error.status, data: { success: false } }));
|
||||
}
|
||||
|
||||
reply({ success: true });
|
||||
},
|
||||
});
|
||||
}
|
|
@ -500,6 +500,82 @@ export class SavedObjectsRepository {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases a counter field by one. Creates the document if one doesn't exist for the given id.
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} id
|
||||
* @param {string} counterFieldName
|
||||
* @param {object} [options={}]
|
||||
* @property {object} [options.migrationVersion=undefined]
|
||||
* @returns {promise}
|
||||
*/
|
||||
async incrementCounter(type, id, counterFieldName, options = {}) {
|
||||
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 {
|
||||
migrationVersion,
|
||||
namespace,
|
||||
} = options;
|
||||
|
||||
const time = this._getCurrentTime();
|
||||
|
||||
|
||||
const migrated = this._migrator.migrateDocument({
|
||||
id,
|
||||
type,
|
||||
attributes: { [counterFieldName]: 1 },
|
||||
migrationVersion,
|
||||
updated_at: time,
|
||||
});
|
||||
|
||||
const raw = this._serializer.savedObjectToRaw(migrated);
|
||||
|
||||
const response = await this._writeToCluster('update', {
|
||||
id: this._serializer.generateRawId(namespace, type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
_source: true,
|
||||
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;
|
||||
}
|
||||
ctx._source.updated_at = params.time;
|
||||
`,
|
||||
lang: 'painless',
|
||||
params: {
|
||||
count: 1,
|
||||
time,
|
||||
type,
|
||||
counterFieldName,
|
||||
},
|
||||
},
|
||||
upsert: raw._source,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
updated_at: time,
|
||||
version: response._version,
|
||||
attributes: response.get._source[type],
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
async _writeToCluster(method, params) {
|
||||
try {
|
||||
await this._onBeforeWrite();
|
||||
|
|
|
@ -1252,6 +1252,219 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#incrementCounter', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.callsFake((method, params) => ({
|
||||
_type: 'doc',
|
||||
_id: params.id,
|
||||
_version: 2,
|
||||
_index: '.kibana',
|
||||
get: {
|
||||
found: true,
|
||||
_source: {
|
||||
type: 'config',
|
||||
...mockTimestampFields,
|
||||
config: {
|
||||
buildNum: 8468,
|
||||
defaultIndex: 'logstash-*',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('formats Elasticsearch response', async () => {
|
||||
callAdminCluster.callsFake((method, params) => ({
|
||||
_type: 'doc',
|
||||
_id: params.id,
|
||||
_version: 2,
|
||||
_index: '.kibana',
|
||||
get: {
|
||||
found: true,
|
||||
_source: {
|
||||
type: 'config',
|
||||
...mockTimestampFields,
|
||||
config: {
|
||||
buildNum: 8468,
|
||||
defaultIndex: 'logstash-*',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const response = await savedObjectsRepository.incrementCounter(
|
||||
'config',
|
||||
'6.0.0-alpha1',
|
||||
'buildNum',
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
type: 'config',
|
||||
id: '6.0.0-alpha1',
|
||||
...mockTimestampFields,
|
||||
version: 2,
|
||||
attributes: {
|
||||
buildNum: 8468,
|
||||
defaultIndex: 'logstash-*'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('migrates the doc if an upsert is required', async () => {
|
||||
migrator.migrateDocument = (doc) => {
|
||||
doc.attributes.buildNum = 42;
|
||||
doc.migrationVersion = { foo: '2.3.4' };
|
||||
return doc;
|
||||
};
|
||||
|
||||
await savedObjectsRepository.incrementCounter(
|
||||
'config',
|
||||
'doesnotexist',
|
||||
'buildNum',
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
expect(callAdminCluster.firstCall.args[1]).toMatchObject({
|
||||
body: {
|
||||
upsert: {
|
||||
config: { buildNum: 42 },
|
||||
migrationVersion: { foo: '2.3.4' },
|
||||
type: 'config',
|
||||
...mockTimestampFields
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => {
|
||||
await savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 'buildNum', {
|
||||
namespace: 'foo-namespace',
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
|
||||
const requestDoc = callAdminCluster.firstCall.args[1];
|
||||
expect(requestDoc.id).toBe('foo-namespace:config:6.0.0-alpha1');
|
||||
expect(requestDoc.body.script.params.type).toBe('config');
|
||||
expect(requestDoc.body.upsert.type).toBe('config');
|
||||
expect(requestDoc).toHaveProperty('body.upsert.config');
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => {
|
||||
await savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 'buildNum');
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
|
||||
const requestDoc = callAdminCluster.firstCall.args[1];
|
||||
expect(requestDoc.id).toBe('config:6.0.0-alpha1');
|
||||
expect(requestDoc.body.script.params.type).toBe('config');
|
||||
expect(requestDoc.body.upsert.type).toBe('config');
|
||||
expect(requestDoc).toHaveProperty('body.upsert.config');
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => {
|
||||
callAdminCluster.callsFake((method, params) => ({
|
||||
_type: 'doc',
|
||||
_id: params.id,
|
||||
_version: 2,
|
||||
_index: '.kibana',
|
||||
get: {
|
||||
found: true,
|
||||
_source: {
|
||||
type: 'globaltype',
|
||||
...mockTimestampFields,
|
||||
globaltype: {
|
||||
counter: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await savedObjectsRepository.incrementCounter('globaltype', 'foo', 'counter', {
|
||||
namespace: 'foo-namespace',
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
|
||||
const requestDoc = callAdminCluster.firstCall.args[1];
|
||||
expect(requestDoc.id).toBe('globaltype:foo');
|
||||
expect(requestDoc.body.script.params.type).toBe('globaltype');
|
||||
expect(requestDoc.body.upsert.type).toBe('globaltype');
|
||||
expect(requestDoc).toHaveProperty('body.upsert.globaltype');
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('should assert that the "type" and "counterFieldName" arguments are strings', () => {
|
||||
expect.assertions(6);
|
||||
|
||||
expect(savedObjectsRepository.incrementCounter(
|
||||
null,
|
||||
'6.0.0-alpha1',
|
||||
'buildNum',
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}),
|
||||
).rejects.toEqual(new Error('"type" argument must be a string'));
|
||||
|
||||
expect(savedObjectsRepository.incrementCounter(
|
||||
42,
|
||||
'6.0.0-alpha1',
|
||||
'buildNum',
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}),
|
||||
).rejects.toEqual(new Error('"type" argument must be a string'));
|
||||
|
||||
expect(savedObjectsRepository.incrementCounter(
|
||||
{},
|
||||
'6.0.0-alpha1',
|
||||
'buildNum',
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}),
|
||||
).rejects.toEqual(new Error('"type" argument must be a string'));
|
||||
|
||||
expect(savedObjectsRepository.incrementCounter(
|
||||
'config',
|
||||
'6.0.0-alpha1',
|
||||
null,
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}),
|
||||
).rejects.toEqual(new Error('"counterFieldName" argument must be a string'));
|
||||
|
||||
expect(savedObjectsRepository.incrementCounter(
|
||||
'config',
|
||||
'6.0.0-alpha1',
|
||||
42,
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}),
|
||||
).rejects.toEqual(new Error('"counterFieldName" argument must be a string'));
|
||||
|
||||
expect(savedObjectsRepository.incrementCounter(
|
||||
'config',
|
||||
'6.0.0-alpha1',
|
||||
{},
|
||||
{
|
||||
namespace: 'foo-namespace',
|
||||
}),
|
||||
).rejects.toEqual(new Error('"counterFieldName" argument must be a string'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBeforeWrite', () => {
|
||||
it('blocks calls to callCluster of requests', async () => {
|
||||
onBeforeWrite.returns(delay(500));
|
||||
|
|
|
@ -21,6 +21,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { uiModules } from '../../modules';
|
||||
import { documentationLinks } from '../../documentation_links/documentation_links';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiButtonEmpty,
|
||||
|
@ -62,8 +63,18 @@ module.directive('queryPopover', function (localStorage) {
|
|||
}
|
||||
|
||||
function onSwitchChange() {
|
||||
$scope.$evalAsync(() => {
|
||||
const newLanguage = $scope.language === 'lucene' ? 'kuery' : 'lucene';
|
||||
|
||||
// Send telemetry info every time the user opts in or out of kuery
|
||||
// As a result it is important this function only ever gets called in the
|
||||
// UI component's change handler.
|
||||
kfetch({
|
||||
pathname: '/api/kibana/kql_opt_in_telemetry',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ opt_in: newLanguage === 'kuery' }),
|
||||
});
|
||||
|
||||
$scope.$evalAsync(() => {
|
||||
localStorage.set('kibana.userQueryLanguage', newLanguage);
|
||||
$scope.onSelectLanguage({ $language: newLanguage });
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./general'));
|
||||
loadTestFile(require.resolve('./home'));
|
||||
loadTestFile(require.resolve('./index_patterns'));
|
||||
loadTestFile(require.resolve('./kql_telemetry'));
|
||||
loadTestFile(require.resolve('./management'));
|
||||
loadTestFile(require.resolve('./saved_objects'));
|
||||
loadTestFile(require.resolve('./scripts'));
|
||||
|
|
24
test/api_integration/apis/kql_telemetry/index.js
Normal file
24
test/api_integration/apis/kql_telemetry/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('KQL', () => {
|
||||
loadTestFile(require.resolve('./kql_telemetry'));
|
||||
});
|
||||
}
|
124
test/api_integration/apis/kql_telemetry/kql_telemetry.js
Normal file
124
test/api_integration/apis/kql_telemetry/kql_telemetry.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 expect from 'expect.js';
|
||||
import Promise from 'bluebird';
|
||||
import { get } from 'lodash';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
describe('telemetry API', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should increment the opt *in* counter in the .kibana/kql-telemetry document', async () => {
|
||||
await supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: true })
|
||||
.expect(200);
|
||||
|
||||
return es.search({
|
||||
index: '.kibana',
|
||||
q: 'type:kql-telemetry',
|
||||
}).then(response => {
|
||||
const kqlTelemetryDoc = get(response, 'hits.hits[0]._source.kql-telemetry');
|
||||
expect(kqlTelemetryDoc.optInCount).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should increment the opt *out* counter in the .kibana/kql-telemetry document', async () => {
|
||||
await supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: false })
|
||||
.expect(200);
|
||||
|
||||
return es.search({
|
||||
index: '.kibana',
|
||||
q: 'type:kql-telemetry',
|
||||
}).then(response => {
|
||||
const kqlTelemetryDoc = get(response, 'hits.hits[0]._source.kql-telemetry');
|
||||
expect(kqlTelemetryDoc.optOutCount).to.be(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should report success when opt *in* is incremented successfully', () => {
|
||||
return supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: true })
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body.success).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report success when opt *out* is incremented successfully', () => {
|
||||
return supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: false })
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body.success).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should only accept literal boolean values for the opt_in POST body param', function () {
|
||||
return Promise.all([
|
||||
supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: 'notabool' })
|
||||
.expect(400),
|
||||
supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: 0 })
|
||||
.expect(400),
|
||||
supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: null })
|
||||
.expect(400),
|
||||
supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ opt_in: undefined })
|
||||
.expect(400),
|
||||
supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.send({})
|
||||
.expect(400),
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -7,5 +7,6 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('kibana', () => {
|
||||
loadTestFile(require.resolve('./stats'));
|
||||
loadTestFile(require.resolve('./kql_telemetry'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('KQL', () => {
|
||||
loadTestFile(require.resolve('./kql_telemetry'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertestNoAuth = getService('supertestWithoutAuth');
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('telemetry API', () => {
|
||||
before(() => esArchiver.load('empty_kibana'));
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('no auth', () => {
|
||||
it('should return 401', async () => {
|
||||
return supertestNoAuth
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.set('kbn-xsrf', 'much access')
|
||||
.send({ opt_in: true })
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with auth', () => {
|
||||
it('should return 200 for a successful request', async () => {
|
||||
return supertest
|
||||
.post('/api/kibana/kql_opt_in_telemetry')
|
||||
.set('content-type', 'application/json')
|
||||
.set('kbn-xsrf', 'such token, wow')
|
||||
.send({ opt_in: true })
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body.success).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue