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: usage analytics report for project types status - ON HOLD - DO NOT MERGE!!! #315

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/app-studio-toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@
"dependencies": {
"@sap/artifact-management": "1.34.0",
"@sap/bas-sdk": "3.8.1",
"@sap/swa-for-sapbas-vsx": "2.0.4",
"@vscode-logging/wrapper": "1.0.1",
"axios": "1.3.5",
"body-parser": "1.20.2",
Expand Down
15 changes: 12 additions & 3 deletions packages/app-studio-toolkit/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import {
deactivateBasRemoteExplorer,
initBasRemoteExplorer,
} from "./devspace-manager/instance";
import { shouldRunCtlServer } from "./utils/bas-utils";
import { isRunOnBAS, reportProjectTypesToUsageAnalytics } from "./utils/bas-utils";
import { AnalyticsWrapper } from "./usage-report/usage-analytics-wrapper";

export function activate(context: ExtensionContext): BasToolkit {
initLogger(context);

// should be trigered earlier on acivating because the `shouldRunCtlServer` method sets the context value of `ext.runPlatform`
if (shouldRunCtlServer()) {
const isInBas = isRunOnBAS();
// should be trigered earlier on acivating because the `isRunOnBAS` method sets the context value of `ext.runPlatform`
if (isInBas) {
getLogger().debug("starting basctl server");
startBasctlServer(context);
}
Expand All @@ -36,6 +38,13 @@ export function activate(context: ExtensionContext): BasToolkit {

initBasRemoteExplorer(context);

if (isInBas) {
setTimeout(() => {
AnalyticsWrapper.createTracker(context.extensionPath);
void reportProjectTypesToUsageAnalytics(basToolkitAPI);
});
}

const logger = getLogger().getChildLogger({ label: "activate" });
logger.info("The App-Studio-Toolkit Extension is active.");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
initTelemetrySettings,
BASClientFactory,
BASTelemetryClient,
} from "@sap/swa-for-sapbas-vsx";
import { join } from "path";
import { readFileSync } from "fs";
import { getLogger } from "../logger/logger";

type Properties = { [key: string]: string | boolean };
type Measurements = { [key: string]: number };
type ProjectData = { [typeName: string]: number };

/**
* A Simple Wrapper for reporting usage analytics
*/
export class AnalyticsWrapper {
private static readonly EVENT_TYPES = {
PROJECT_TYPES_STATUS: "Project Types Status",
};

public static getTracker(): BASTelemetryClient {
return BASClientFactory.getBASTelemetryClient();
}

public static createTracker(extensionPath: string): void {
try {
const packageJsonPath = join(extensionPath, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const vscodeExtentionFullName = `${packageJson.publisher}.${packageJson.name}`;
initTelemetrySettings(vscodeExtentionFullName, packageJson.version);
getLogger().info(
`SAP Web Analytics tracker was created for ${vscodeExtentionFullName}`
);
} catch (error: any) {
getLogger().error(error);
}
}

private static report(opt: {
eventName: string;
properties: Properties;
measurements?: Measurements;
}): void {
// We want to report only if we are not in Local VSCode environment
if (process.env.LANDSCAPE_ENVIRONMENT) {
void AnalyticsWrapper.getTracker().report(
opt.eventName,
{ ...opt.properties },
{ ...opt.measurements }
);
getLogger().trace("SAP Web Analytics tracker was called", {
eventName: opt.eventName,
});
}
}

public static traceProjectTypesStatus(
devSpacePackName: string,
projects: ProjectData
): void {
try {
const eventName = AnalyticsWrapper.EVENT_TYPES.PROJECT_TYPES_STATUS;
for (const projectType in projects) {
AnalyticsWrapper.report({
eventName,
properties: { projectType , devSpacePackName },
measurements: { projectTypeQuantity: projects[projectType] },
});
}
} catch (error: any) {
getLogger().error(error);
}
}
}

export { BASClientFactory, BASTelemetryClient };
45 changes: 43 additions & 2 deletions packages/app-studio-toolkit/src/utils/bas-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ExtensionKind, commands, env, extensions } from "vscode";
import { join, split, tail } from "lodash";
import { join, split, tail, countBy, compact } from "lodash";
import { devspace } from "@sap/bas-sdk";
import { URL } from "node:url";
import { ProjectData } from "@sap/artifact-management";
import { BasToolkit, sam } from "@sap-devx/app-studio-toolkit-types";
import { AnalyticsWrapper } from "../usage-report/usage-analytics-wrapper";
import { getLogger } from "../logger/logger";

export enum ExtensionRunMode {
desktop = `desktop`,
Expand All @@ -12,7 +16,9 @@ export enum ExtensionRunMode {
unexpected = `unexpected`,
}

export function shouldRunCtlServer(): boolean {
type ProjectTypeMap = { [type: string]: number } | undefined;

export function isRunOnBAS(): boolean {
const platform = getExtensionRunPlatform();
return (
platform === ExtensionRunMode.basWorkspace || // BAS
Expand Down Expand Up @@ -58,3 +64,38 @@ function getExtensionRunPlatform(): ExtensionRunMode {

return runPlatform;
}

export async function reportProjectTypesToUsageAnalytics(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing the function return type

basToolkitAPI: BasToolkit
): Promise<void> {
const [devspaceInfo, projects] = await Promise.all([
devspace.getDevspaceInfo(),
getProjectsInfo(basToolkitAPI),
]);
if (devspaceInfo?.packDisplayName && projects) {
void AnalyticsWrapper.traceProjectTypesStatus(
devspaceInfo.packDisplayName,
projects
);
}
}

/*
There are 2 issues that should be fixes before merging this PR:
1. getProjectInfo API should return all projects under all structure. including deep nested projects.
2. the projects found are not cached in the WorksapceApi object. The new ProjectApi objects will be re-calculated out every time the WorkspaceApi.getProjects method is called.
*/
async function getProjectsInfo(basToolkitAPI: BasToolkit): Promise<ProjectTypeMap> {
try {
const workspaceAPI = basToolkitAPI.workspaceAPI;
const projects: sam.ProjectApi[] = await workspaceAPI.getProjects();
const folderProjectInfoList: ProjectData[] = compact(await Promise.all(
projects.map(
async (project: sam.ProjectApi) => project.getProjectInfo()
)
));
return countBy(folderProjectInfoList, "typeName");
} catch (error) {
getLogger().error(`Failed to get project list`);
}
}
12 changes: 6 additions & 6 deletions packages/app-studio-toolkit/test/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe("extension unit test", () => {
let sandbox: SinonSandbox;
let workspaceMock: SinonMock;
let basctlServerMock: SinonMock;
let shouldRunCtlServerMock: SinonMock;
let isRunOnBASMock: SinonMock;
let performerMock: SinonMock;
let wsConfigMock: SinonMock;
let loggerMock: SinonMock;
Expand All @@ -78,7 +78,7 @@ describe("extension unit test", () => {
beforeEach(() => {
workspaceMock = sandbox.mock(testVscode.workspace);
basctlServerMock = sandbox.mock(basctlServer);
shouldRunCtlServerMock = sandbox.mock(runInBas);
isRunOnBASMock = sandbox.mock(runInBas);
performerMock = sandbox.mock(performer);
wsConfigMock = sandbox.mock(wsConfig);
loggerMock = sandbox.mock(logger);
Expand All @@ -88,7 +88,7 @@ describe("extension unit test", () => {
afterEach(() => {
workspaceMock.verify();
basctlServerMock.verify();
shouldRunCtlServerMock.verify();
isRunOnBASMock.verify();
performerMock.verify();
wsConfigMock.verify();
loggerMock.verify();
Expand Down Expand Up @@ -139,7 +139,7 @@ describe("extension unit test", () => {
.expects("initBasRemoteExplorer")
.withExactArgs(context);
loggerMock.expects("initLogger").withExactArgs(context);
shouldRunCtlServerMock.expects("shouldRunCtlServer").returns(true);
isRunOnBASMock.expects("isRunOnBAS").once().returns(true);
basctlServerMock.expects("startBasctlServer");
const scheduledAction = {
name: "actName",
Expand All @@ -164,7 +164,7 @@ describe("extension unit test", () => {
};

loggerMock.expects("initLogger").withExactArgs(context);
shouldRunCtlServerMock.expects("shouldRunCtlServer").returns(false);
isRunOnBASMock.expects("isRunOnBAS").once().returns(false);
performerMock.expects("_performAction").never();

wsConfigMock.expects("get").withExactArgs("actions", []).returns([]);
Expand All @@ -183,7 +183,7 @@ describe("extension unit test", () => {
const testError = new Error("Socket failure");

loggerMock.expects("initLogger").withExactArgs(context);
shouldRunCtlServerMock.expects("shouldRunCtlServer").returns(true);
isRunOnBASMock.expects("isRunOnBAS").returns(true);
basctlServerMock.expects("startBasctlServer").throws(testError);

try {
Expand Down
133 changes: 133 additions & 0 deletions packages/app-studio-toolkit/test/usage-analytics-wrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import chai from "chai";
import sinon from "sinon";
const { expect } = chai;

import {
AnalyticsWrapper,
BASClientFactory,
BASTelemetryClient,
} from "../src/usage-report/usage-analytics-wrapper";
import * as logger from "../src/logger/logger";

describe("AnalyticsWrapper", () => {
let sandbox: sinon.SinonSandbox;
let getBASTelemetryClientStub: any;
let mockClient: any;
let getLoggerStub: any;

beforeEach(() => {
sandbox = sinon.createSandbox();
mockClient = sandbox.createStubInstance(BASTelemetryClient);
mockClient.report = sandbox.stub();
getBASTelemetryClientStub = sandbox.stub(
BASClientFactory,
"getBASTelemetryClient"
);
getBASTelemetryClientStub.returns(mockClient);

getLoggerStub = sandbox.stub(logger, "getLogger").returns({
info: sandbox.stub(),
error: sandbox.stub(),
changeLevel: sandbox.stub(),
changeSourceLocationTracking: sandbox.stub(),
fatal: sandbox.stub(),
warn: sandbox.stub(),
debug: sandbox.stub(),
trace: sandbox.stub(),
getChildLogger: sandbox.stub(),
});
});

afterEach(() => {
sandbox.restore();
});

describe("getTracker", () => {
it("should return a BASTelemetryClient instance", () => {
const tracker = AnalyticsWrapper.getTracker();
expect(tracker).to.be.an.instanceOf(BASTelemetryClient);
});
});

describe("createTracker", () => {
it("should create a tracker", () => {
const extensionPath = ".";
AnalyticsWrapper.createTracker(extensionPath);
expect(
getLoggerStub().info.calledOnceWith(
`SAP Web Analytics tracker was created for SAPOSS.app-studio-toolkit`
)
).to.be.true;
});

it("should handle errors while creating a tracker", () => {
const extensionPath = "/wrong/path/to/extension";
AnalyticsWrapper.createTracker(extensionPath);
expect(getLoggerStub().error.calledOnce).to.be.true;
});
});

describe("traceProjectTypesStatus", () => {
it("LANDSCAPE_ENVIRONMENT does not exist, should not report", () => {
delete process.env.LANDSCAPE_ENVIRONMENT;

const devSpacePackName = "devSpacePackName";
const projects = {
"com.sap.cap": 2,
};

AnalyticsWrapper.traceProjectTypesStatus(devSpacePackName, projects);
expect(mockClient.report.callCount).to.equal(0);
expect(getLoggerStub().trace.callCount).to.equal(0);
});

it("should report project types status correctly", () => {
process.env.LANDSCAPE_ENVIRONMENT = "true";

const devSpacePackName = "devSpacePackName";
const projects = {
"CAP": 2,
"UI5": 1,
"Fiori Freestyle": 3,
};

AnalyticsWrapper.traceProjectTypesStatus(devSpacePackName, projects);

expect(mockClient.report.callCount).to.equal(3);
expect(
mockClient.report.calledWith(
"Project Types Status",
{ projectType: "CAP", devSpacePackName },
{ projectTypeQuantity: 2 }
)
).to.be.true;
expect(
mockClient.report.calledWith(
"Project Types Status",
{ projectType: "UI5", devSpacePackName },
{ projectTypeQuantity: 1 }
)
).to.be.true;
expect(
mockClient.report.calledWith(
"Project Types Status",
{ projectType: "Fiori Freestyle", devSpacePackName },
{ projectTypeQuantity: 3 }
)
).to.be.true;
});

it("report throw error", () => {
const err = new Error("report error");
mockClient.report.throws(err);

const devSpacePackName = "devSpacePackName";
const projects = {
"CAP": 2,
};

AnalyticsWrapper.traceProjectTypesStatus(devSpacePackName, projects);
expect(getLoggerStub().error.calledOnceWith(err)).to.be.true;
});
});
});
Loading
Loading