Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fiori-gen): add telemetry helper class and associated functions #2489

Merged
merged 19 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/polite-tables-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/fiori-generator-shared': minor
---

adds new functions
6 changes: 5 additions & 1 deletion packages/fiori-generator-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@
"dependencies": {
"@sap-ux/btp-utils": "workspace:*",
"@sap-ux/project-access": "workspace:*",
"@sap-ux/telemetry": "workspace:*",
"@vscode-logging/logger": "2.0.0",
"i18next": "20.6.1",
"logform": "2.4.0",
"mem-fs": "2.1.0",
"mem-fs-editor": "9.4.0"
"mem-fs-editor": "9.4.0",
"os-name": "4.0.1",
"semver": "7.5.4"
},
"devDependencies": {
"@types/mem-fs-editor": "7.0.1",
"@types/mem-fs": "1.1.2",
"@types/semver": "7.5.2",
"@types/yeoman-environment": "2.10.11"
},
"engines": {
Expand Down
15 changes: 15 additions & 0 deletions packages/fiori-generator-shared/src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { isAppStudio } from '@sap-ux/btp-utils';
import { hostEnvironment } from './types';

/**
* Determine if the current prompting environment is cli .
*
Expand All @@ -10,3 +13,15 @@ export function isCli(): boolean {
return false;
}
}

/**
* Determine if the current prompting environment is cli or a hosted extension (app studio or vscode).
*
* @returns the platform name and technical name
*/
export function getHostEnvironment(): { name: string; technical: string } {
cianmSAP marked this conversation as resolved.
Show resolved Hide resolved
if (isCli()) {
return hostEnvironment.cli;
}
return isAppStudio() ? hostEnvironment.bas : hostEnvironment.vscode;
}
9 changes: 6 additions & 3 deletions packages/fiori-generator-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export * from './cap';
export * from './environment';
export * from './logWrapper';
export * from './system-utils';
export * from './telemetry';
export { getPackageScripts } from './getPackageScripts';
export { getBootstrapResourceUrls, getDefaultTargetFolder } from './helpers';
export { generateReadMe } from './read-me';
export * from './system-utils';
export { PackageJsonScripts, YeomanEnvironment, VSCodeInstance } from './types';
export * from './logWrapper';
export { getHostEnvironment } from './environment';
export { isExtensionInstalled } from './installedCheck';
export { PackageJsonScripts, YeomanEnvironment, VSCodeInstance, hostEnvironment } from './types';
22 changes: 22 additions & 0 deletions packages/fiori-generator-shared/src/installedCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { coerce, lt } from 'semver';

/**
* Check for an installed extension, optionally specifying a minimum version.
* Note, this does not check for activation state of specified extension.
*
* @param vscode - vscode instance
* @param extensionId - the id of the extension to find
* @param minVersion - the minimum version of the specified extension, lower versions will not be returned. Must be a valid SemVer string.
* @returns true if the extension is installed and the version is >= minVersion (if provided), false otherwise
*/
export function isExtensionInstalled(vscode: any, extensionId: string, minVersion?: string): boolean {
IainSAP marked this conversation as resolved.
Show resolved Hide resolved
const foundExt = vscode?.extensions?.getExtension(extensionId);
if (foundExt) {
const extVersion = coerce(foundExt.packageJSON.version);
if (extVersion) {
// Check installed ver is >= minVersion or return true if minVersion is not specified
return !(minVersion && lt(extVersion, minVersion));
}
}
return false;
}
2 changes: 2 additions & 0 deletions packages/fiori-generator-shared/src/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { sendTelemetry, sendTelemetryBlocking } from './utils';
export * from './telemetryHelper';
103 changes: 103 additions & 0 deletions packages/fiori-generator-shared/src/telemetry/telemetryHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
PerformanceMeasurementAPI as Performance,
initTelemetrySettings,
type TelemetryProperties,
type ToolsSuiteTelemetryInitSettings
} from '@sap-ux/telemetry';
import type { TelemetryData } from './types';
import { getHostEnvironment } from '../environment';
import osName from 'os-name';
import { t } from '../i18n';

/**
* Helper class for intialising and preparing event data for telemetry.
*/
export abstract class TelemetryHelper {
IainSAP marked this conversation as resolved.
Show resolved Hide resolved
private static _telemetryData: TelemetryData;
private static _previousEventTimestamp: number;

/**
* Returns the telemetry data.
*
* @returns telemetry data
*/
public static get telemetryData(): TelemetryData {
return this._telemetryData;
}

/**
* Load telemetry settings.
*
* @param options - tools suite telemetry init settings
*/
public static async initTelemetrySettings(options: ToolsSuiteTelemetryInitSettings): Promise<void> {
await initTelemetrySettings(options);
}

/**
* Creates telemetry data and adds default telemetry props.
*
* @param additionalData - set additional properties to be reported by telemetry
* @param filterDups - filters duplicates by returning undefined if it's suspected to be a repeated event based on previous telemetry data & timestamp (1 second)
* @returns telemetry data
*/
public static createTelemetryData<T extends TelemetryProperties>(
additionalData?: Partial<T>,
filterDups = false
): TelemetryData | undefined {
const currentTimestamp = new Date().getTime();
if (!this._previousEventTimestamp) {
filterDups = false; // can't filter duplicates if no previous event timestamp
this._previousEventTimestamp = currentTimestamp;
}

if (!this._telemetryData) {
let osVersionName = t('telemetry.unknownOs');
try {
osVersionName = osName();
} catch {
// no matched os name, possible beta or unreleased version
}
this._telemetryData = {
Platform: getHostEnvironment().technical,
OperatingSystem: osVersionName
};
}

if (filterDups) {
const newTelemData = { ...this._telemetryData, ...additionalData };
if (
Math.abs(this._previousEventTimestamp - currentTimestamp) < 1000 &&
JSON.stringify(newTelemData) === JSON.stringify(this._telemetryData)
longieirl marked this conversation as resolved.
Show resolved Hide resolved
) {
return undefined;
}
}
this._previousEventTimestamp = currentTimestamp;
this._telemetryData = Object.assign(this._telemetryData, additionalData);

return this._telemetryData;
}

/**
* Marks the start time. Example usage:
* At the start of of the writing phase of the yeoman generator.
* It should not be updated everytime calling createTelemetryData().
*/
public static markAppGenStartTime(): void {
TelemetryHelper.createTelemetryData({
markName: Performance.startMark('LOADING_TIME')
});
}

/**
* Marks the end time. Example usage:
* At the end of the writing phase of yeoman generator.
*/
public static markAppGenEndTime(): void {
if (this._telemetryData?.markName) {
Performance.endMark(this._telemetryData.markName);
Performance.measure(this._telemetryData.markName);
}
}
}
3 changes: 3 additions & 0 deletions packages/fiori-generator-shared/src/telemetry/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface TelemetryData {
[key: string]: string;
}
67 changes: 67 additions & 0 deletions packages/fiori-generator-shared/src/telemetry/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ClientFactory, PerformanceMeasurementAPI as Performance, SampleRate } from '@sap-ux/telemetry';
import type { TelemetryEvent } from '@sap-ux/telemetry';
import type { TelemetryData } from './types';
import { TelemetryHelper } from './telemetryHelper';

/**
* Prepares the telemetry event to be sent to the telemetry client.
*
* @param telemetryEventName - the event name to be reported
* @param telemetryData - the telemetry data
* @returns - the telemetry event
*/
function prepareTelemetryEvent(telemetryEventName: string, telemetryData: TelemetryData): TelemetryEvent {
// Make sure performance measurement end is called
TelemetryHelper.markAppGenEndTime();
const generationTime = telemetryData.markName
? Performance.getMeasurementDuration(telemetryData.markName)
: undefined;

return {
eventName: telemetryEventName,
properties: telemetryData,
measurements: generationTime ? { GenerationTime: generationTime } : {}
};
}

/**
* Sends the telemetry event to the telemetry client.
*
* @param telemetryEventName - the event name to be reported
* @param telemetryData - the telemetry data
* @param appPath - the path of the application
* @returns - a promise that resolves when the event is sent
*/
export async function sendTelemetry(
telemetryEventName: string,
telemetryData: TelemetryData,
appPath?: string
): Promise<void> {
const telemetryEvent = prepareTelemetryEvent(telemetryEventName, telemetryData);
return ClientFactory.getTelemetryClient().reportEvent(
telemetryEvent,
SampleRate.NoSampling,
appPath ? { appPath } : undefined
);
}

/**
* Sends the telemetry event to the telemetry client and blocks the execution until the event is sent.
*
* @param telemetryEventName - the event name to be reported
* @param telemetryData - the telemetry data
* @param appPath - the path of the application
* @returns - a promise that resolves when the event is sent
*/
export async function sendTelemetryBlocking(
telemetryEventName: string,
telemetryData: TelemetryData,
appPath?: string
): Promise<void> {
const telemetryEvent = prepareTelemetryEvent(telemetryEventName, telemetryData);
return ClientFactory.getTelemetryClient().reportEventBlocking(
telemetryEvent,
SampleRate.NoSampling,
appPath ? { appPath } : undefined
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
},
"debug": {
"loggingConfigured": "Logging has been configured at log level: {{logLevel}}"
},
"telemetry": {
"unknownOs": "Unknown"
longieirl marked this conversation as resolved.
Show resolved Hide resolved
}
}
20 changes: 20 additions & 0 deletions packages/fiori-generator-shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,23 @@ export interface PackageScriptsOptions {
/** If true, a script for starting the app without flp will be generated. Defaults to true. */
generateIndex?: boolean;
}

export const hostEnvironment = {
vscode: {
name: 'Visual Studio Code',
technical: 'VSCode'
},
bas: {
name: 'SAP Business Application Studio',
technical: 'SBAS'
},
cli: {
name: 'CLI',
technical: 'CLI'
}
};

export enum ApiHubType {
apiHub = 'API_HUB',
apiHubEnterprise = 'API_HUB_ENTERPRISE'
}
59 changes: 59 additions & 0 deletions packages/fiori-generator-shared/test/environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { hostEnvironment } from '../src/types';
import { isCli, getHostEnvironment } from '../src/environment';
import * as btpUtils from '@sap-ux/btp-utils';

jest.mock('@sap-ux/btp-utils', () => {
return {
__esModule: true,
...jest.requireActual('@sap-ux/btp-utils')
};
});

function mockCli(isCli: boolean) {
process.argv[1] = isCli ? 'path/to/yo' : 'path/to/mock';
process.stdin.isTTY = isCli ? true : false;
}

describe('environment utils', () => {
const originalArgv = process.argv;

beforeEach(() => {
jest.resetAllMocks();
process.argv = [...originalArgv];
});

afterEach(() => {
process.argv = originalArgv;
});

afterAll(() => {
process.stdin.destroy();
});

it('should return true for cli', () => {
mockCli(true);
expect(isCli()).toBe(true);
});

it('should return false for non-cli', () => {
mockCli(false);
expect(isCli()).toBe(false);
});

it('should return correct host environment - cli', () => {
mockCli(true);
expect(getHostEnvironment()).toEqual(hostEnvironment.cli);
});

it('should return correct host environment - app studio', () => {
mockCli(false);
jest.spyOn(btpUtils, 'isAppStudio').mockReturnValueOnce(true);
expect(getHostEnvironment()).toEqual(hostEnvironment.bas);
});

it('should return correct host environment - vscode', () => {
mockCli(false);
jest.spyOn(btpUtils, 'isAppStudio').mockReturnValueOnce(false);
expect(getHostEnvironment()).toEqual(hostEnvironment.vscode);
});
});
22 changes: 22 additions & 0 deletions packages/fiori-generator-shared/test/installedCheck.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { isExtensionInstalled } from '../src/installedCheck';

describe('Installed module checker', () => {
test('isExtensionInstalled', () => {
expect(isExtensionInstalled(undefined, 'wont.be.found.extension')).toBe(false);

const mockVSCodeRef = {
extensions: {
getExtension: () => ({
packageJSON: {
version: '1.2.3'
}
})
}
};

expect(isExtensionInstalled(mockVSCodeRef, 'will.be.found.extension')).toBe(true);
expect(isExtensionInstalled(mockVSCodeRef, 'version.not.satisfied.extension', '1.2.4')).toBe(false);
expect(isExtensionInstalled(mockVSCodeRef, 'version.equal.extension', '1.2.3')).toBe(true);
expect(isExtensionInstalled(mockVSCodeRef, 'version.lower.extension', '0.0.1')).toBe(true);
});
});
Loading
Loading