[Spaces] Telemetry for space solution property (#184192)

## Summary

Added telemetry for space solution property.

### How to test
```
# Create a couple of spaces with solution
POST kbn:/api/spaces/space 
{
  "name": "space with solution",
  "id": "my-space-solution-1",
  "description": "a description",
  "color": "#5c5959",
  "solution": "search",
  "disabledFeatures": []
}

POST kbn:/api/spaces/space 
{
  "name": "space with solution",
  "id": "my-space-solution-2",
  "description": "a description",
  "color": "#5c5959",
  "solution": "search",
  "disabledFeatures": []
}

POST kbn:/api/spaces/space 
{
  "name": "space with solution",
  "id": "my-space-solution-3",
  "description": "a description",
  "color": "#5c5959",
  "solution": "security",
  "disabledFeatures": []
}

# Get stats data
POST kbn:/internal/telemetry/clusters/_stats
{
  "unencrypted": true, "refreshCache": true
}
```


### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

__Fixes: https://github.com/elastic/kibana/issues/183641__

## Release Note
Added telemetry for space solution property.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
elena-shostak 2024-06-03 13:47:37 +02:00 committed by GitHub
parent 10f61743ad
commit b4238544ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 5 deletions

View file

@ -10410,7 +10410,7 @@
"description": "Non-default value of setting."
}
},
"observability:enableInfrastructureContainerAssetView":{
"observability:enableInfrastructureContainerAssetView": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."

View file

@ -112,6 +112,14 @@ const getMockedEsClient = () => {
},
],
},
solution: {
buckets: [
{
key: 'search',
doc_count: 5,
},
],
},
},
});
return esClient;
@ -161,6 +169,7 @@ describe('with a basic license', () => {
disabledFeatures: {
terms: { field: 'space.disabledFeatures', include: ['feature1', 'feature2'], size: 2 },
},
solution: { terms: { field: 'space.solution', missing: 'unset', size: 5 } },
},
query: { term: { type: { value: 'space' } } },
size: 0,

View file

@ -46,6 +46,8 @@ async function getSpacesUsage(
}
const knownFeatureIds = features.getKibanaFeatures().map((feature) => feature.id);
const knownSolutions = ['classic', 'search', 'observability', 'security', 'unset'];
const resp = (await esClient.search({
index: kibanaIndex,
body: {
@ -65,6 +67,13 @@ async function getSpacesUsage(
size: knownFeatureIds.length,
},
},
solution: {
terms: {
field: 'space.solution',
size: knownSolutions.length,
missing: 'unset',
},
},
},
size: 0,
},
@ -74,11 +83,17 @@ async function getSpacesUsage(
const count = hits?.total?.value ?? 0;
const disabledFeatureBuckets = aggregations?.disabledFeatures?.buckets ?? [];
const solutionBuckets = aggregations?.solution?.buckets ?? [];
const initialCounts = knownFeatureIds.reduce((acc, featureId) => {
const initialCounts = knownFeatureIds.reduce<Record<string, number>>((acc, featureId) => {
acc[featureId] = 0;
return acc;
}, {} as Record<string, number>);
}, {});
const initialSolutionCounts = knownSolutions.reduce<Record<string, number>>((acc, solution) => {
acc[solution] = 0;
return acc;
}, {});
const disabledFeatures: Record<string, number> = disabledFeatureBuckets.reduce(
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -89,6 +104,15 @@ async function getSpacesUsage(
initialCounts
);
const solutions = solutionBuckets.reduce<Record<string, number>>(
// eslint-disable-next-line @typescript-eslint/naming-convention
(acc, { key, doc_count }) => {
acc[key] = doc_count;
return acc;
},
initialSolutionCounts
);
const usesFeatureControls = Object.values(disabledFeatures).some(
(disabledSpaceCount) => disabledSpaceCount > 0
);
@ -97,6 +121,7 @@ async function getSpacesUsage(
count,
usesFeatureControls,
disabledFeatures,
solutions,
} as UsageData;
}
@ -117,6 +142,7 @@ export interface UsageData extends UsageStats {
enabled: boolean;
count?: number;
usesFeatureControls?: boolean;
solutions: Record<string, number>;
disabledFeatures: {
// "feature": number;
[key: string]: number | undefined;
@ -172,6 +198,38 @@ export function getSpacesUsageCollector(
'Indicates if at least one feature is disabled in at least one space. This is a signal that space-level feature controls are in use. This does not account for role-based (security) feature controls.',
},
},
solutions: {
classic: {
type: 'long',
_meta: {
description: 'The number of spaces which have solution set to classic.',
},
},
search: {
type: 'long',
_meta: {
description: 'The number of spaces which have solution set to search.',
},
},
observability: {
type: 'long',
_meta: {
description: 'The number of spaces which have solution set to observability.',
},
},
security: {
type: 'long',
_meta: {
description: 'The number of spaces which have solution set to security.',
},
},
unset: {
type: 'long',
_meta: {
description: 'The number of spaces without solution set.',
},
},
},
disabledFeatures: {
// "feature": number;
DYNAMIC_KEY: {

View file

@ -15113,6 +15113,40 @@
"description": "Indicates if at least one feature is disabled in at least one space. This is a signal that space-level feature controls are in use. This does not account for role-based (security) feature controls."
}
},
"solutions": {
"properties": {
"classic": {
"type": "long",
"_meta": {
"description": "The number of spaces which have solution set to classic."
}
},
"search": {
"type": "long",
"_meta": {
"description": "The number of spaces which have solution set to search."
}
},
"observability": {
"type": "long",
"_meta": {
"description": "The number of spaces which have solution set to observability."
}
},
"security": {
"type": "long",
"_meta": {
"description": "The number of spaces which have solution set to security."
}
},
"unset": {
"type": "long",
"_meta": {
"description": "The number of spaces without solution set."
}
}
}
},
"disabledFeatures": {
"properties": {
"DYNAMIC_KEY": {

View file

@ -14,7 +14,7 @@ export default function ({ getService }: FtrProviderContext) {
const usageAPI = getService('usageAPI');
describe('Verify disabledFeatures telemetry payloads', async () => {
beforeEach(async () => {
before(async () => {
await spacesService.create({
id: 'space-1',
name: 'space-1',
@ -28,13 +28,24 @@ export default function ({ getService }: FtrProviderContext) {
name: 'space-2',
description: 'This is your space-2!',
color: '#00bfb3',
solution: 'security',
disabledFeatures: ['savedObjectsManagement', 'canvas', 'maps'],
});
await spacesService.create({
id: 'space-3',
name: 'space-3',
description: 'This is your space-3!',
color: '#00bfb3',
disabledFeatures: [],
solution: 'search',
});
});
afterEach(async () => {
after(async () => {
await spacesService.delete('space-1');
await spacesService.delete('space-2');
await spacesService.delete('space-3');
});
it('includes only disabledFeatures findings', async () => {
@ -83,5 +94,20 @@ export default function ({ getService }: FtrProviderContext) {
savedQueryManagement: 0,
});
});
it('includes only solution findings', async () => {
const [{ stats }] = await usageAPI.getTelemetryStats({
unencrypted: true,
refreshCache: true,
});
expect(stats.stack_stats.kibana.plugins.spaces.solutions).to.eql({
security: 1,
search: 1,
observability: 0,
classic: 0,
unset: 2,
});
});
});
}