mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
a2d6f1ad6c
commit
02b6ceaff5
14 changed files with 1645 additions and 83 deletions
|
@ -7,7 +7,6 @@
|
|||
"src/plugins/testbed/",
|
||||
"src/plugins/kibana_utils/",
|
||||
"src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts",
|
||||
"src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts",
|
||||
"src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts",
|
||||
"src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts",
|
||||
"src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts"
|
||||
|
|
|
@ -5,6 +5,22 @@
|
|||
"flat": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"my_index_signature_prop": {
|
||||
"properties": {
|
||||
"avg": {
|
||||
"type": "number"
|
||||
},
|
||||
"count": {
|
||||
"type": "number"
|
||||
},
|
||||
"max": {
|
||||
"type": "number"
|
||||
},
|
||||
"min": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"my_str": {
|
||||
"type": "text"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { SyntaxKind } from 'typescript';
|
||||
import { ParsedUsageCollection } from '../ts_parser';
|
||||
|
||||
export const parsedIndexedInterfaceWithNoMatchingSchema: ParsedUsageCollection = [
|
||||
'src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts',
|
||||
{
|
||||
collectorName: 'indexed_interface_with_not_matching_schema',
|
||||
schema: {
|
||||
value: {
|
||||
something: {
|
||||
count_1: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fetch: {
|
||||
typeName: 'Usage',
|
||||
typeDescriptor: {
|
||||
'': {
|
||||
'@@INDEX@@': {
|
||||
count_1: {
|
||||
kind: SyntaxKind.NumberKeyword,
|
||||
type: 'NumberKeyword',
|
||||
},
|
||||
count_2: {
|
||||
kind: SyntaxKind.NumberKeyword,
|
||||
type: 'NumberKeyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -32,6 +32,20 @@ export const parsedWorkingCollector: ParsedUsageCollection = [
|
|||
my_str: {
|
||||
type: 'text',
|
||||
},
|
||||
my_index_signature_prop: {
|
||||
avg: {
|
||||
type: 'number',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
},
|
||||
max: {
|
||||
type: 'number',
|
||||
},
|
||||
min: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
my_objects: {
|
||||
total: {
|
||||
type: 'number',
|
||||
|
@ -60,6 +74,14 @@ export const parsedWorkingCollector: ParsedUsageCollection = [
|
|||
kind: SyntaxKind.StringKeyword,
|
||||
type: 'StringKeyword',
|
||||
},
|
||||
my_index_signature_prop: {
|
||||
'': {
|
||||
'@@INDEX@@': {
|
||||
kind: SyntaxKind.NumberKeyword,
|
||||
type: 'NumberKeyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
my_objects: {
|
||||
total: {
|
||||
kind: SyntaxKind.NumberKeyword,
|
||||
|
|
|
@ -90,6 +90,38 @@ Array [
|
|||
},
|
||||
},
|
||||
],
|
||||
Array [
|
||||
"src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts",
|
||||
Object {
|
||||
"collectorName": "indexed_interface_with_not_matching_schema",
|
||||
"fetch": Object {
|
||||
"typeDescriptor": Object {
|
||||
"": Object {
|
||||
"@@INDEX@@": Object {
|
||||
"count_1": Object {
|
||||
"kind": 140,
|
||||
"type": "NumberKeyword",
|
||||
},
|
||||
"count_2": Object {
|
||||
"kind": 140,
|
||||
"type": "NumberKeyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"typeName": "Usage",
|
||||
},
|
||||
"schema": Object {
|
||||
"value": Object {
|
||||
"something": Object {
|
||||
"count_1": Object {
|
||||
"type": "long",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
Array [
|
||||
"src/fixtures/telemetry_collectors/nested_collector.ts",
|
||||
Object {
|
||||
|
@ -132,6 +164,14 @@ Array [
|
|||
"type": "BooleanKeyword",
|
||||
},
|
||||
},
|
||||
"my_index_signature_prop": Object {
|
||||
"": Object {
|
||||
"@@INDEX@@": Object {
|
||||
"kind": 140,
|
||||
"type": "NumberKeyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
"my_objects": Object {
|
||||
"total": Object {
|
||||
"kind": 140,
|
||||
|
@ -166,6 +206,20 @@ Array [
|
|||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"my_index_signature_prop": Object {
|
||||
"avg": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"count": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"max": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"min": Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"my_objects": Object {
|
||||
"total": Object {
|
||||
"type": "number",
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
import * as ts from 'typescript';
|
||||
import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
|
||||
import { parsedIndexedInterfaceWithNoMatchingSchema } from './__fixture__/parsed_indexed_interface_with_not_matching_schema';
|
||||
import { checkCompatibleTypeDescriptor, checkMatchingMapping } from './check_collector_integrity';
|
||||
import * as path from 'path';
|
||||
import { readFile } from 'fs';
|
||||
|
@ -82,6 +83,20 @@ describe('checkCompatibleTypeDescriptor', () => {
|
|||
expect(incompatibles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns diff on indexed interface with no matching schema', () => {
|
||||
const incompatibles = checkCompatibleTypeDescriptor([
|
||||
parsedIndexedInterfaceWithNoMatchingSchema,
|
||||
]);
|
||||
expect(incompatibles).toHaveLength(1);
|
||||
const { diff, message } = incompatibles[0];
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
expect(diff).toEqual({ '.@@INDEX@@.count_2.kind': 'number' });
|
||||
expect(message).toHaveLength(1);
|
||||
expect(message).toEqual([
|
||||
'incompatible Type key (Usage..@@INDEX@@.count_2): expected (undefined) got ("number").',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Interface Change', () => {
|
||||
it('returns diff on incompatible type descriptor with mapping', () => {
|
||||
const malformedParsedCollector = cloneDeep(parsedWorkingCollector);
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('extractCollectors', () => {
|
|||
const programPaths = await getProgramPaths(configs[0]);
|
||||
|
||||
const results = [...extractCollectors(programPaths, tsConfig)];
|
||||
expect(results).toHaveLength(6);
|
||||
expect(results).toHaveLength(7);
|
||||
expect(results).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -84,6 +84,11 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor |
|
|||
}, {} as any);
|
||||
}
|
||||
|
||||
// If it's defined as signature { [key: string]: OtherInterface }
|
||||
if (ts.isIndexSignatureDeclaration(node) && node.type) {
|
||||
return { '@@INDEX@@': getDescriptor(node.type, program) };
|
||||
}
|
||||
|
||||
if (ts.SyntaxKind.FirstNode === node.kind) {
|
||||
return getDescriptor((node as any).right, program);
|
||||
}
|
||||
|
|
|
@ -98,6 +98,14 @@ export function getVariableValue(node: ts.Node): string | Record<string, any> {
|
|||
return serializeObject(node);
|
||||
}
|
||||
|
||||
if (ts.isIdentifier(node)) {
|
||||
const declaration = getIdentifierDeclaration(node);
|
||||
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
||||
return getVariableValue(declaration.initializer);
|
||||
}
|
||||
// TODO: If this is another imported value from another file, we'll need to go fetch it like in getPropertyValue
|
||||
}
|
||||
|
||||
throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`);
|
||||
}
|
||||
|
||||
|
@ -112,10 +120,11 @@ export function serializeObject(node: ts.Node) {
|
|||
if (typeof propertyName === 'undefined') {
|
||||
throw new Error(`Unable to get property name ${property.getText()}`);
|
||||
}
|
||||
const cleanPropertyName = propertyName.replace(/["']/g, '');
|
||||
if (ts.isPropertyAssignment(property)) {
|
||||
value[propertyName] = getVariableValue(property.initializer);
|
||||
value[cleanPropertyName] = getVariableValue(property.initializer);
|
||||
} else {
|
||||
value[propertyName] = getVariableValue(property);
|
||||
value[cleanPropertyName] = getVariableValue(property);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,9 +231,29 @@ export const flattenKeys = (obj: any, keyPath: any[] = []): any => {
|
|||
};
|
||||
|
||||
export function difference(actual: any, expected: any) {
|
||||
function changes(obj: any, base: any) {
|
||||
function changes(obj: { [key: string]: any }, base: { [key: string]: any }) {
|
||||
return transform(obj, function (result, value, key) {
|
||||
if (key && !isEqual(value, base[key])) {
|
||||
if (key && /@@INDEX@@/.test(`${key}`)) {
|
||||
// The type definition is an Index Signature, fuzzy searching for similar keys
|
||||
const regexp = new RegExp(`${key}`.replace(/@@INDEX@@/g, '(.+)?'));
|
||||
const keysInBase = Object.keys(base)
|
||||
.map((k) => {
|
||||
const match = k.match(regexp);
|
||||
return match && match[0];
|
||||
})
|
||||
.filter((s): s is string => !!s);
|
||||
|
||||
if (keysInBase.length === 0) {
|
||||
// Mark this key as wrong because we couldn't find any matching keys
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
keysInBase.forEach((k) => {
|
||||
if (!isEqual(value, base[k])) {
|
||||
result[k] = isObject(value) && isObject(base[k]) ? changes(value, base[k]) : value;
|
||||
}
|
||||
});
|
||||
} else if (key && !isEqual(value, base[key])) {
|
||||
result[key] = isObject(value) && isObject(base[key]) ? changes(value, base[key]) : value;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
|
||||
import { loggerMock } from '../../core/server/logging/logger.mock';
|
||||
|
||||
const { makeUsageCollector } = new CollectorSet({
|
||||
logger: loggerMock.create(),
|
||||
maximumWaitTimeForAllCollectorsInS: 0,
|
||||
});
|
||||
|
||||
interface Usage {
|
||||
[key: string]: {
|
||||
count_1?: number;
|
||||
count_2?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const myCollector = makeUsageCollector<Usage>({
|
||||
type: 'indexed_interface_with_not_matching_schema',
|
||||
isReady: () => true,
|
||||
fetch() {
|
||||
if (Math.random()) {
|
||||
return { something: { count_1: 1 } };
|
||||
}
|
||||
return { something: { count_2: 2 } };
|
||||
},
|
||||
schema: {
|
||||
something: {
|
||||
count_1: { type: 'long' }, // Intentionally missing count_2
|
||||
},
|
||||
},
|
||||
});
|
|
@ -35,6 +35,9 @@ interface Usage {
|
|||
my_objects: MyObject;
|
||||
my_array?: MyObject[];
|
||||
my_str_array?: string[];
|
||||
my_index_signature_prop?: {
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
const SOME_NUMBER: number = 123;
|
||||
|
@ -93,5 +96,11 @@ export const myCollector = makeUsageCollector<Usage>({
|
|||
type: { type: 'boolean' },
|
||||
},
|
||||
my_str_array: { type: 'keyword' },
|
||||
my_index_signature_prop: {
|
||||
count: { type: 'number' },
|
||||
avg: { type: 'number' },
|
||||
max: { type: 'number' },
|
||||
min: { type: 'number' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { MakeSchemaFrom } from 'src/plugins/usage_collection/server';
|
||||
import { ApplicationUsageTelemetryReport } from './telemetry_application_usage_collector';
|
||||
|
||||
const commonSchema: MakeSchemaFrom<ApplicationUsageTelemetryReport[string]> = {
|
||||
clicks_total: {
|
||||
type: 'long',
|
||||
},
|
||||
clicks_7_days: {
|
||||
type: 'long',
|
||||
},
|
||||
clicks_30_days: {
|
||||
type: 'long',
|
||||
},
|
||||
clicks_90_days: {
|
||||
type: 'long',
|
||||
},
|
||||
minutes_on_screen_total: {
|
||||
type: 'float',
|
||||
},
|
||||
minutes_on_screen_7_days: {
|
||||
type: 'float',
|
||||
},
|
||||
minutes_on_screen_30_days: {
|
||||
type: 'float',
|
||||
},
|
||||
minutes_on_screen_90_days: {
|
||||
type: 'float',
|
||||
},
|
||||
};
|
||||
|
||||
// These keys obtained by searching for `/application\w*\.register\(/` and checking the value of the attr `id`.
|
||||
// TODO: Find a way to update these keys automatically.
|
||||
export const applicationUsageSchema = {
|
||||
// OSS
|
||||
dashboards: commonSchema,
|
||||
dev_tools: commonSchema,
|
||||
discover: commonSchema,
|
||||
home: commonSchema,
|
||||
kibana: commonSchema, // It's a forward app so we'll likely never report it
|
||||
management: commonSchema,
|
||||
short_url_redirect: commonSchema, // It's a forward app so we'll likely never report it
|
||||
timelion: commonSchema,
|
||||
visualize: commonSchema,
|
||||
|
||||
// X-Pack
|
||||
apm: commonSchema,
|
||||
csm: commonSchema,
|
||||
canvas: commonSchema,
|
||||
dashboard_mode: commonSchema, // It's a forward app so we'll likely never report it
|
||||
appSearch: commonSchema,
|
||||
workplaceSearch: commonSchema,
|
||||
graph: commonSchema,
|
||||
logs: commonSchema,
|
||||
metrics: commonSchema,
|
||||
infra: commonSchema, // It's a forward app so we'll likely never report it
|
||||
ingestManager: commonSchema,
|
||||
lens: commonSchema,
|
||||
maps: commonSchema,
|
||||
ml: commonSchema,
|
||||
monitoring: commonSchema,
|
||||
'observability-overview': commonSchema,
|
||||
security_account: commonSchema,
|
||||
security_access_agreement: commonSchema,
|
||||
security_capture_url: commonSchema, // It's a forward app so we'll likely never report it
|
||||
security_logged_out: commonSchema,
|
||||
security_login: commonSchema,
|
||||
security_logout: commonSchema,
|
||||
security_overwritten_session: commonSchema,
|
||||
securitySolution: commonSchema, // It's a forward app so we'll likely never report it
|
||||
'securitySolution:overview': commonSchema,
|
||||
'securitySolution:detections': commonSchema,
|
||||
'securitySolution:hosts': commonSchema,
|
||||
'securitySolution:network': commonSchema,
|
||||
'securitySolution:timelines': commonSchema,
|
||||
'securitySolution:case': commonSchema,
|
||||
'securitySolution:administration': commonSchema,
|
||||
siem: commonSchema,
|
||||
space_selector: commonSchema,
|
||||
uptime: commonSchema,
|
||||
};
|
|
@ -26,6 +26,7 @@ import {
|
|||
ApplicationUsageTransactional,
|
||||
registerMappings,
|
||||
} from './saved_objects_types';
|
||||
import { applicationUsageSchema } from './schema';
|
||||
|
||||
/**
|
||||
* Roll indices every 24h
|
||||
|
@ -40,7 +41,7 @@ export const ROLL_INDICES_START = 5 * 60 * 1000;
|
|||
export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals';
|
||||
export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional';
|
||||
|
||||
interface ApplicationUsageTelemetryReport {
|
||||
export interface ApplicationUsageTelemetryReport {
|
||||
[appId: string]: {
|
||||
clicks_total: number;
|
||||
clicks_7_days: number;
|
||||
|
@ -60,93 +61,96 @@ export function registerApplicationUsageCollector(
|
|||
) {
|
||||
registerMappings(registerType);
|
||||
|
||||
const collector = usageCollection.makeUsageCollector({
|
||||
type: 'application_usage',
|
||||
isReady: () => typeof getSavedObjectsClient() !== 'undefined',
|
||||
fetch: async () => {
|
||||
const savedObjectsClient = getSavedObjectsClient();
|
||||
if (typeof savedObjectsClient === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([
|
||||
findAll<ApplicationUsageTotal>(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }),
|
||||
findAll<ApplicationUsageTransactional>(savedObjectsClient, {
|
||||
type: SAVED_OBJECTS_TRANSACTIONAL_TYPE,
|
||||
}),
|
||||
]);
|
||||
const collector = usageCollection.makeUsageCollector<ApplicationUsageTelemetryReport | undefined>(
|
||||
{
|
||||
type: 'application_usage',
|
||||
isReady: () => typeof getSavedObjectsClient() !== 'undefined',
|
||||
schema: applicationUsageSchema,
|
||||
fetch: async () => {
|
||||
const savedObjectsClient = getSavedObjectsClient();
|
||||
if (typeof savedObjectsClient === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([
|
||||
findAll<ApplicationUsageTotal>(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }),
|
||||
findAll<ApplicationUsageTransactional>(savedObjectsClient, {
|
||||
type: SAVED_OBJECTS_TRANSACTIONAL_TYPE,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationUsageFromTotals = rawApplicationUsageTotals.reduce(
|
||||
(acc, { attributes: { appId, minutesOnScreen, numberOfClicks } }) => {
|
||||
const existing = acc[appId] || { clicks_total: 0, minutes_on_screen_total: 0 };
|
||||
return {
|
||||
...acc,
|
||||
[appId]: {
|
||||
clicks_total: numberOfClicks + existing.clicks_total,
|
||||
const applicationUsageFromTotals = rawApplicationUsageTotals.reduce(
|
||||
(acc, { attributes: { appId, minutesOnScreen, numberOfClicks } }) => {
|
||||
const existing = acc[appId] || { clicks_total: 0, minutes_on_screen_total: 0 };
|
||||
return {
|
||||
...acc,
|
||||
[appId]: {
|
||||
clicks_total: numberOfClicks + existing.clicks_total,
|
||||
clicks_7_days: 0,
|
||||
clicks_30_days: 0,
|
||||
clicks_90_days: 0,
|
||||
minutes_on_screen_total: minutesOnScreen + existing.minutes_on_screen_total,
|
||||
minutes_on_screen_7_days: 0,
|
||||
minutes_on_screen_30_days: 0,
|
||||
minutes_on_screen_90_days: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as ApplicationUsageTelemetryReport
|
||||
);
|
||||
const nowMinus7 = moment().subtract(7, 'days');
|
||||
const nowMinus30 = moment().subtract(30, 'days');
|
||||
const nowMinus90 = moment().subtract(90, 'days');
|
||||
|
||||
const applicationUsage = rawApplicationUsageTransactional.reduce(
|
||||
(acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => {
|
||||
const existing = acc[appId] || {
|
||||
clicks_total: 0,
|
||||
clicks_7_days: 0,
|
||||
clicks_30_days: 0,
|
||||
clicks_90_days: 0,
|
||||
minutes_on_screen_total: minutesOnScreen + existing.minutes_on_screen_total,
|
||||
minutes_on_screen_total: 0,
|
||||
minutes_on_screen_7_days: 0,
|
||||
minutes_on_screen_30_days: 0,
|
||||
minutes_on_screen_90_days: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as ApplicationUsageTelemetryReport
|
||||
);
|
||||
const nowMinus7 = moment().subtract(7, 'days');
|
||||
const nowMinus30 = moment().subtract(30, 'days');
|
||||
const nowMinus90 = moment().subtract(90, 'days');
|
||||
};
|
||||
|
||||
const applicationUsage = rawApplicationUsageTransactional.reduce(
|
||||
(acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => {
|
||||
const existing = acc[appId] || {
|
||||
clicks_total: 0,
|
||||
clicks_7_days: 0,
|
||||
clicks_30_days: 0,
|
||||
clicks_90_days: 0,
|
||||
minutes_on_screen_total: 0,
|
||||
minutes_on_screen_7_days: 0,
|
||||
minutes_on_screen_30_days: 0,
|
||||
minutes_on_screen_90_days: 0,
|
||||
};
|
||||
const timeOfEntry = moment(timestamp as string);
|
||||
const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7);
|
||||
const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30);
|
||||
const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90);
|
||||
|
||||
const timeOfEntry = moment(timestamp as string);
|
||||
const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7);
|
||||
const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30);
|
||||
const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90);
|
||||
const last7Days = {
|
||||
clicks_7_days: existing.clicks_7_days + numberOfClicks,
|
||||
minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen,
|
||||
};
|
||||
const last30Days = {
|
||||
clicks_30_days: existing.clicks_30_days + numberOfClicks,
|
||||
minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen,
|
||||
};
|
||||
const last90Days = {
|
||||
clicks_90_days: existing.clicks_90_days + numberOfClicks,
|
||||
minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen,
|
||||
};
|
||||
|
||||
const last7Days = {
|
||||
clicks_7_days: existing.clicks_7_days + numberOfClicks,
|
||||
minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen,
|
||||
};
|
||||
const last30Days = {
|
||||
clicks_30_days: existing.clicks_30_days + numberOfClicks,
|
||||
minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen,
|
||||
};
|
||||
const last90Days = {
|
||||
clicks_90_days: existing.clicks_90_days + numberOfClicks,
|
||||
minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen,
|
||||
};
|
||||
return {
|
||||
...acc,
|
||||
[appId]: {
|
||||
...existing,
|
||||
clicks_total: existing.clicks_total + numberOfClicks,
|
||||
minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen,
|
||||
...(isInLast7Days ? last7Days : {}),
|
||||
...(isInLast30Days ? last30Days : {}),
|
||||
...(isInLast90Days ? last90Days : {}),
|
||||
},
|
||||
};
|
||||
},
|
||||
applicationUsageFromTotals
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[appId]: {
|
||||
...existing,
|
||||
clicks_total: existing.clicks_total + numberOfClicks,
|
||||
minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen,
|
||||
...(isInLast7Days ? last7Days : {}),
|
||||
...(isInLast30Days ? last30Days : {}),
|
||||
...(isInLast90Days ? last90Days : {}),
|
||||
},
|
||||
};
|
||||
},
|
||||
applicationUsageFromTotals
|
||||
);
|
||||
|
||||
return applicationUsage;
|
||||
},
|
||||
});
|
||||
return applicationUsage;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
usageCollection.registerCollector(collector);
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue