Skip to content

Commit d1f1774

Browse files
committed
[Usage Collection] [schema] Support spreads + canvas definition (#78481)
# Conflicts: # src/plugins/usage_collection/server/collector/collector.ts
1 parent 1ba7d22 commit d1f1774

14 files changed

Lines changed: 457 additions & 55 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { SyntaxKind } from 'typescript';
21+
import { ParsedUsageCollection } from '../ts_parser';
22+
23+
export const parsedSchemaDefinedWithSpreadsCollector: ParsedUsageCollection = [
24+
'src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts',
25+
{
26+
collectorName: 'schema_defined_with_spreads',
27+
schema: {
28+
value: {
29+
flat: {
30+
type: 'keyword',
31+
},
32+
my_str: {
33+
type: 'text',
34+
},
35+
my_objects: {
36+
total: {
37+
type: 'number',
38+
},
39+
type: {
40+
type: 'boolean',
41+
},
42+
},
43+
},
44+
},
45+
fetch: {
46+
typeName: 'Usage',
47+
typeDescriptor: {
48+
flat: {
49+
kind: SyntaxKind.StringKeyword,
50+
type: 'StringKeyword',
51+
},
52+
my_str: {
53+
kind: SyntaxKind.StringKeyword,
54+
type: 'StringKeyword',
55+
},
56+
my_objects: {
57+
total: {
58+
kind: SyntaxKind.NumberKeyword,
59+
type: 'NumberKeyword',
60+
},
61+
type: {
62+
kind: SyntaxKind.BooleanKeyword,
63+
type: 'BooleanKeyword',
64+
},
65+
},
66+
},
67+
},
68+
},
69+
];

packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('extractCollectors', () => {
3434
const programPaths = await getProgramPaths(configs[0]);
3535

3636
const results = [...extractCollectors(programPaths, tsConfig)];
37-
expect(results).toHaveLength(7);
37+
expect(results).toHaveLength(8);
3838
expect(results).toMatchSnapshot();
3939
});
4040
});

packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { parsedNestedCollector } from './__fixture__/parsed_nested_collector';
2525
import { parsedExternallyDefinedCollector } from './__fixture__/parsed_externally_defined_collector';
2626
import { parsedImportedUsageInterface } from './__fixture__/parsed_imported_usage_interface';
2727
import { parsedImportedSchemaCollector } from './__fixture__/parsed_imported_schema';
28+
import { parsedSchemaDefinedWithSpreadsCollector } from './__fixture__/parsed_schema_defined_with_spreads_collector';
2829

2930
export function loadFixtureProgram(fixtureName: string) {
3031
const fixturePath = path.resolve(
@@ -62,6 +63,12 @@ describe('parseUsageCollection', () => {
6263
expect(result).toEqual([parsedWorkingCollector]);
6364
});
6465

66+
it('parses collector with schema defined as union of spreads', () => {
67+
const { program, sourceFile } = loadFixtureProgram('schema_defined_with_spreads_collector');
68+
const result = [...parseUsageCollection(sourceFile, program)];
69+
expect(result).toEqual([parsedSchemaDefinedWithSpreadsCollector]);
70+
});
71+
6572
it('parses nested collectors', () => {
6673
const { program, sourceFile } = loadFixtureProgram('nested_collector');
6774
const result = [...parseUsageCollection(sourceFile, program)];

packages/kbn-telemetry-tools/src/tools/utils.ts

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -100,42 +100,55 @@ export function getIdentifierDeclaration(node: ts.Node) {
100100
return getIdentifierDeclarationFromSource(node, source);
101101
}
102102

103-
export function getVariableValue(node: ts.Node): string | Record<string, any> {
103+
export function getVariableValue(node: ts.Node, program: ts.Program): string | Record<string, any> {
104104
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
105105
return node.text;
106106
}
107107

108108
if (ts.isObjectLiteralExpression(node)) {
109-
return serializeObject(node);
109+
return serializeObject(node, program);
110110
}
111111

112112
if (ts.isIdentifier(node)) {
113113
const declaration = getIdentifierDeclaration(node);
114114
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
115-
return getVariableValue(declaration.initializer);
115+
return getVariableValue(declaration.initializer, program);
116+
} else {
117+
// Go fetch it in another file
118+
return getIdentifierValue(node, node, program, { chaseImport: true });
116119
}
117-
// TODO: If this is another imported value from another file, we'll need to go fetch it like in getPropertyValue
118120
}
119121

120-
throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`);
122+
if (ts.isSpreadAssignment(node)) {
123+
return getVariableValue(node.expression, program);
124+
}
125+
126+
throw Error(
127+
`Unsupported Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`
128+
);
121129
}
122130

123-
export function serializeObject(node: ts.Node) {
131+
export function serializeObject(node: ts.Node, program: ts.Program) {
124132
if (!ts.isObjectLiteralExpression(node)) {
125133
throw new Error(`Expecting Object literal Expression got ${node.getText()}`);
126134
}
127135

128-
const value: Record<string, any> = {};
136+
let value: Record<string, any> = {};
129137
for (const property of node.properties) {
130138
const propertyName = property.name?.getText();
139+
const val = ts.isPropertyAssignment(property)
140+
? getVariableValue(property.initializer, program)
141+
: getVariableValue(property, program);
142+
131143
if (typeof propertyName === 'undefined') {
132-
throw new Error(`Unable to get property name ${property.getText()}`);
133-
}
134-
const cleanPropertyName = propertyName.replace(/["']/g, '');
135-
if (ts.isPropertyAssignment(property)) {
136-
value[cleanPropertyName] = getVariableValue(property.initializer);
144+
if (typeof val === 'object') {
145+
value = { ...value, ...val };
146+
} else {
147+
throw new Error(`Unable to get property name ${property.getText()}`);
148+
}
137149
} else {
138-
value[cleanPropertyName] = getVariableValue(property);
150+
const cleanPropertyName = propertyName.replace(/["']/g, '');
151+
value[cleanPropertyName] = val;
139152
}
140153
}
141154

@@ -155,45 +168,53 @@ export function getResolvedModuleSourceFile(
155168
return resolvedModuleSourceFile;
156169
}
157170

158-
export function getPropertyValue(
171+
export function getIdentifierValue(
159172
node: ts.Node,
173+
initializer: ts.Identifier,
160174
program: ts.Program,
161175
config: Optional<{ chaseImport: boolean }> = {}
162176
) {
163177
const { chaseImport = false } = config;
178+
const identifierName = initializer.getText();
179+
const declaration = getIdentifierDeclaration(initializer);
180+
if (ts.isImportSpecifier(declaration)) {
181+
if (!chaseImport) {
182+
throw new Error(
183+
`Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.`
184+
);
185+
}
164186

165-
if (ts.isPropertyAssignment(node)) {
166-
const { initializer } = node;
187+
const importedModuleName = getModuleSpecifier(declaration);
167188

168-
if (ts.isIdentifier(initializer)) {
169-
const identifierName = initializer.getText();
170-
const declaration = getIdentifierDeclaration(initializer);
171-
if (ts.isImportSpecifier(declaration)) {
172-
if (!chaseImport) {
173-
throw new Error(
174-
`Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.`
175-
);
176-
}
189+
const source = node.getSourceFile();
190+
const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
191+
const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource);
192+
if (!ts.isVariableDeclaration(declarationNode)) {
193+
throw new Error(`Expected ${identifierName} to be variable declaration.`);
194+
}
195+
if (!declarationNode.initializer) {
196+
throw new Error(`Expected ${identifierName} to be initialized.`);
197+
}
198+
const serializedObject = serializeObject(declarationNode.initializer, program);
199+
return serializedObject;
200+
}
177201

178-
const importedModuleName = getModuleSpecifier(declaration);
202+
return getVariableValue(declaration, program);
203+
}
179204

180-
const source = node.getSourceFile();
181-
const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
182-
const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource);
183-
if (!ts.isVariableDeclaration(declarationNode)) {
184-
throw new Error(`Expected ${identifierName} to be variable declaration.`);
185-
}
186-
if (!declarationNode.initializer) {
187-
throw new Error(`Expected ${identifierName} to be initialized.`);
188-
}
189-
const serializedObject = serializeObject(declarationNode.initializer);
190-
return serializedObject;
191-
}
205+
export function getPropertyValue(
206+
node: ts.Node,
207+
program: ts.Program,
208+
config: Optional<{ chaseImport: boolean }> = {}
209+
) {
210+
if (ts.isPropertyAssignment(node)) {
211+
const { initializer } = node;
192212

193-
return getVariableValue(declaration);
213+
if (ts.isIdentifier(initializer)) {
214+
return getIdentifierValue(node, initializer, program, config);
194215
}
195216

196-
return getVariableValue(initializer);
217+
return getVariableValue(initializer, program);
197218
}
198219
}
199220

src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ export const myCollector = makeUsageCollector<Usage>({
4141
return { something: { count_2: 2 } };
4242
},
4343
schema: {
44+
// @ts-expect-error Intentionally missing count_2
4445
something: {
45-
count_1: { type: 'long' }, // Intentionally missing count_2
46+
count_1: { type: 'long' },
4647
},
4748
},
4849
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { CollectorSet, MakeSchemaFrom } from '../../plugins/usage_collection/server/collector';
20+
import { loggerMock } from '../../core/server/logging/logger.mock';
21+
22+
const { makeUsageCollector } = new CollectorSet({
23+
logger: loggerMock.create(),
24+
maximumWaitTimeForAllCollectorsInS: 0,
25+
});
26+
27+
interface MyObject {
28+
total: number;
29+
type: boolean;
30+
}
31+
32+
interface Usage {
33+
flat?: string;
34+
my_str?: string;
35+
my_objects: MyObject;
36+
}
37+
38+
const SOME_NUMBER: number = 123;
39+
40+
const someSchema: MakeSchemaFrom<Pick<Usage, 'flat' | 'my_str'>> = {
41+
flat: {
42+
type: 'keyword',
43+
},
44+
my_str: {
45+
type: 'text',
46+
},
47+
};
48+
49+
const someOtherSchema: MakeSchemaFrom<Pick<Usage, 'my_objects'>> = {
50+
my_objects: {
51+
total: {
52+
type: 'number',
53+
},
54+
type: { type: 'boolean' },
55+
},
56+
};
57+
58+
export const myCollector = makeUsageCollector<Usage>({
59+
type: 'schema_defined_with_spreads',
60+
isReady: () => true,
61+
fetch() {
62+
const testString = '123';
63+
64+
return {
65+
flat: 'hello',
66+
my_str: testString,
67+
my_objects: {
68+
total: SOME_NUMBER,
69+
type: true,
70+
},
71+
};
72+
},
73+
schema: {
74+
...someSchema,
75+
...someOtherSchema,
76+
},
77+
});

0 commit comments

Comments
 (0)