Skip to content

Commit

Permalink
feat(fiori-gen): add telemetry helper class and associated functions (#…
Browse files Browse the repository at this point in the history
…2489)

* feat(fiori-gen): add common utilities

* feat(fiori-gen): changeset

* feat(fiori-gen): fix test

* feat(fiori-gen): lint

* feat(fiori-gen): update odata service inq

* feat(fiori-gen): update gethostenvironment function

* feat(fiori-gen): fix test

* feat(ref-lib): revert odata serivce inq changes

* feat(ref-lib): revert odata serivce inq changes

* feat(ref-lib): lint & export

* Linting auto fix commit

* feat(fiori-gen): update param name

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: IainSAP <[email protected]>
  • Loading branch information
3 people authored Oct 25, 2024
1 parent bb4d1e2 commit 231e713
Show file tree
Hide file tree
Showing 17 changed files with 474 additions and 17 deletions.
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 } {
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 {
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 {
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)
) {
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"
}
}
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

0 comments on commit 231e713

Please sign in to comment.