Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 9 additions & 1 deletion vscode/src/telemetry/events/baseEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { LOGGER } from "../../logger";
import { AnonymousIdManager } from "../impl/AnonymousIdManager";
import { cacheService } from "../impl/cacheServiceImpl";
import { getHashCode } from "../utils";
import { getHashCode, getValuesToBeTransformed, transformValue } from "../utils";

export interface BaseEventPayload {
vsCodeId: string;
Expand All @@ -26,6 +26,7 @@ export interface BaseEventPayload {
export abstract class BaseEvent<T> {
protected _payload: T & BaseEventPayload;
protected _data: T
private static readonly blockedValues = getValuesToBeTransformed();

constructor(public readonly NAME: string,
public readonly ENDPOINT: string,
Expand All @@ -47,6 +48,13 @@ export abstract class BaseEvent<T> {
return this._data;
}

protected static transformEvent = (propertiesToTransform: string[], payload: Record<string, any>): any => {
const replacedValue = "_REM_";

return transformValue(null, this.blockedValues, propertiesToTransform, replacedValue, payload);
};


public onSuccessPostEventCallback = async (): Promise<void> => {
LOGGER.debug(`${this.NAME} sent successfully`);
}
Expand Down
4 changes: 3 additions & 1 deletion vscode/src/telemetry/events/jdkFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export interface JdkFeatureEventData {
export class JdkFeatureEvent extends BaseEvent<JdkFeatureEventData> {
public static readonly NAME = "jdkFeature";
public static readonly ENDPOINT = "/jdkFeature";
private static readonly propertiesToTransform = ['javaVersion'];

constructor(payload: JdkFeatureEventData) {
super(JdkFeatureEvent.NAME, JdkFeatureEvent.ENDPOINT, payload);
const updatedPayload: JdkFeatureEventData = BaseEvent.transformEvent(JdkFeatureEvent.propertiesToTransform, payload);
super(JdkFeatureEvent.NAME, JdkFeatureEvent.ENDPOINT, updatedPayload);
}

public static concatEvents(events:JdkFeatureEvent[]): JdkFeatureEvent[] {
Expand Down
4 changes: 3 additions & 1 deletion vscode/src/telemetry/events/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ export interface StartEventData {
export class ExtensionStartEvent extends BaseEvent<StartEventData> {
public static readonly NAME = "startup";
public static readonly ENDPOINT = "/start";
private static readonly propertiesToTransform = ['osVersion'];

constructor(payload: StartEventData) {
super(ExtensionStartEvent.NAME, ExtensionStartEvent.ENDPOINT, payload);
const updatedPayload: StartEventData = BaseEvent.transformEvent(ExtensionStartEvent.propertiesToTransform, payload);
super(ExtensionStartEvent.NAME, ExtensionStartEvent.ENDPOINT, updatedPayload);
}

onSuccessPostEventCallback = async (): Promise<void> => {
Expand Down
4 changes: 3 additions & 1 deletion vscode/src/telemetry/events/workspaceChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ let workspaceChangeEventTimeout: NodeJS.Timeout | null = null;
export class WorkspaceChangeEvent extends BaseEvent<WorkspaceChangeData> {
public static readonly NAME = "workspaceChange";
public static readonly ENDPOINT = "/workspaceChange";
private static readonly propertiesToTransform = ['javaVersion'];

constructor(payload: WorkspaceChangeData) {
super(WorkspaceChangeEvent.NAME, WorkspaceChangeEvent.ENDPOINT, payload);
const updatedPayload: WorkspaceChangeData = BaseEvent.transformEvent(WorkspaceChangeEvent.propertiesToTransform, payload);
super(WorkspaceChangeEvent.NAME, WorkspaceChangeEvent.ENDPOINT, updatedPayload);
}

public onSuccessPostEventCallback = async (): Promise<void> => {
Expand Down
71 changes: 71 additions & 0 deletions vscode/src/telemetry/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
import * as crypto from 'crypto';
import { Uri, workspace } from 'vscode';
import * as os from 'os';
import { isObject, isString } from '../utils';

export const getCurrentUTCDateInSeconds = () => {
const date = Date.now();
Expand Down Expand Up @@ -70,3 +72,72 @@ const getUri = (pathOrUri: Uri | string): Uri => {
}
return Uri.file(pathOrUri);
}

export const getValuesToBeTransformed = (): string[] => {
const MIN_BLOCKED_VAL_LENGTH = 3;
const IGNORED_VALUES = ['java', 'vscode', 'user', 'oracle'];

const blockedValues: (string | undefined)[] = [
process.env?.SUDO_USER,
process.env?.C9_USER,
process.env?.LOGNAME,
process.env?.USER,
process.env?.LNAME,
process.env?.USERNAME,
process.env?.HOSTNAME,
process.env?.COMPUTERNAME,
process.env?.NAME,
os.userInfo().username].map(el => el?.trim());

return Array.from(new Set(blockedValues.filter((el): el is string => el !== undefined &&
el.length >= MIN_BLOCKED_VAL_LENGTH &&
!IGNORED_VALUES.includes(el)
)));
}


export const isPrimitiveTransformationNotRequired = (value: any) => (
value === null ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'undefined' ||
typeof value === 'symbol' ||
typeof value === 'bigint'
);


export const transformValue = (key: string | null, blockedValues: string[], propertiesToTransform: string[], replacedValue: string, value: any): any => {
if (isPrimitiveTransformationNotRequired(value)) {
return value;
}

if (isString(value)) {
if (!value.trim().length) return value;

let updatedValue = value;
if (key == null || !propertiesToTransform.includes(key)) return value;
blockedValues.forEach(valueToBeReplaced =>
updatedValue = replaceAllOccurrences(updatedValue, valueToBeReplaced, replacedValue)
);

return updatedValue;
}

if (Array.isArray(value)) {
return value.map(el => transformValue(key, blockedValues, propertiesToTransform, replacedValue, el));
}

if (isObject(value)) {
const result: any = {};
for (const [k, val] of Object.entries(value)) {
result[k] = transformValue(k, blockedValues, propertiesToTransform, replacedValue, val);
}
return result;
}
return value;
};

export const replaceAllOccurrences = (str: string, valueString: string, replaceString: string) => {
if(!valueString.trim().length) return str;
return str.split(valueString).join(replaceString);
}
200 changes: 200 additions & 0 deletions vscode/src/test/unit/telemetry/baseEvent.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { expect } from "chai";
import { describe, it, beforeEach, afterEach } from "mocha";
import { transformValue, replaceAllOccurrences, getValuesToBeTransformed } from "../../../telemetry/utils";
import * as sinon from 'sinon';
import * as os from 'os';

const replacedValue = "_REM_";
const propertiesToTransform = ["message", "b", "c", "d"];

describe("transformValue()", () => {
it("replaces top-level string if key is in propertiesToTransform", () => {
const result = transformValue("message", ["john"], propertiesToTransform, replacedValue, "hello john");
expect(result).to.equal(`hello ${replacedValue}`);
});

it("does not replace if key is not in propertiesToTransform", () => {
const result = transformValue("greeting", ["john"], propertiesToTransform, replacedValue, "hello john");
expect(result).to.equal("hello john");
});

it("replaces nested name values for matching keys only", () => {
const input = {
b: "john is here",
c: ["john", { d: "also john" }]
};

const result = transformValue("a", ["john"], propertiesToTransform, replacedValue, input);

expect(result.b).to.equal(`${replacedValue} is here`);
expect(result.c[0]).to.equal(`${replacedValue}`);
expect(result.c[1].d).to.equal(`also ${replacedValue}`);
});

it("handles keys with deep nesting", () => {
const blocked = ["john", "doe"];
const data = {
level1: {
message: "john was here",
test: "john was here",
b: ["hello john", "doe speaks", "java"],
nestedArray: [
{
b: "john and doe",
other: "keep this"
},
{
b: "john is safe",
d: ["john doe", { d: "doe and john" }]
}
]
},
c: [
{
d: "test john",
random: "doe"
},
{
a: {
b: "john"
},
b: "john and jane",
d: null
}
],
extra: "nothing here"
};

const result = transformValue(null, blocked, propertiesToTransform, replacedValue, data);

expect(result.level1.message).to.equal(`${replacedValue} was here`);
expect(result.level1.test).to.equal("john was here");
expect(result.level1.b).to.deep.equal([`hello ${replacedValue}`, `${replacedValue} speaks`, "java"]);
expect(result.level1.nestedArray[0].b).to.equal(`${replacedValue} and ${replacedValue}`);
expect(result.level1.nestedArray[0].other).to.equal("keep this");

expect(result.level1.nestedArray[1].b).to.equal(`${replacedValue} is safe`);
expect(result.level1.nestedArray[1].d[0]).to.equal(`${replacedValue} ${replacedValue}`);
expect(result.level1.nestedArray[1].d[1].d).to.equal(`${replacedValue} and ${replacedValue}`);

expect(result.c[0].d).to.equal(`test ${replacedValue}`);
expect(result.c[0].random).to.equal("doe");

expect(result.c[1].a.b).to.equal(`${replacedValue}`);
expect(result.c[1].b).to.equal(`${replacedValue} and jane`);
expect(result.c[1].d).to.equal(null);

expect(result.extra).to.equal("nothing here");
});

it("does not transform primitive values", () => {
const data = {
count: 5,
flag: true,
nothing: null
};
const result = transformValue(null, ["john"], propertiesToTransform, replacedValue, data);

expect(result.count).to.equal(5);
expect(result.flag).to.equal(true);
expect(result.nothing).to.equal(null);
});

it("returns original string if no blocked values", () => {
const result = transformValue("message", [], propertiesToTransform, replacedValue, "hello john");
expect(result).to.equal("hello john");
});
});

describe("replaceAllOccurrences()", () => {
it("replaces all non-overlapping occurrences", () => {
const result = replaceAllOccurrences("john is john", "john", "_REM_");
expect(result).to.equal("_REM_ is _REM_");
});

it("does not change string if valueString not found", () => {
const result = replaceAllOccurrences("hello world", "john", "_REM_");
expect(result).to.equal("hello world");
});

it("replaces multiple adjacent matches", () => {
const result = replaceAllOccurrences("johnjohnjohn", "john", "_REM_");
expect(result).to.equal("_REM__REM__REM_");
});

it("returns original string if valueString is empty", () => {
const result = replaceAllOccurrences("john is here", "", "_REM_");
expect(result).to.equal("john is here");
});

it("can replace special characters (treated literally)", () => {
const result = replaceAllOccurrences("price is $5.00 and $5.00", "$5.00", "_REM_");
expect(result).to.equal("price is _REM_ and _REM_");
});

it("is case-sensitive by default", () => {
const result = replaceAllOccurrences("John john JOHN", "john", "_REM_");
expect(result).to.equal("John _REM_ JOHN");
});

it("handles overlapping cases (only first per loop)", () => {
const result = replaceAllOccurrences("aaa", "aa", "_X_");
expect(result).to.equal("_X_a");
});
});

describe("transformValue() with getValuesToBeTransformed()", () => {
let stub: sinon.SinonStub;

beforeEach(() => {
stub = sinon.stub(os, "userInfo").returns({ username: "john" } as os.UserInfo<string>);
});

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

it("transforms string using values from getValuesToBeTransformed", () => {
const blocked = getValuesToBeTransformed();

const result = transformValue("message", blocked, propertiesToTransform, replacedValue, "hello john");

expect(result).to.equal(`hello ${replacedValue}`);
});

it("does not transform if key is not in propertiesToTransform", () => {
const blocked = getValuesToBeTransformed();

const result = transformValue("greeting", blocked, propertiesToTransform, replacedValue, "hello john");

expect(result).to.equal("hello john");
});

it("filters ignored and short env values correctly", () => {
const testEnv = {
SUDO_USER: "ja",
C9_USER: "java",
LOGNAME: "john",
USER: "oracle",
LNAME: "ab",
USERNAME: "johndoe",
HOSTNAME: undefined,
COMPUTERNAME: "user",
NAME: "vscode"
};

const stubbed = sinon.stub(process, "env").value(testEnv as any);
stub.restore();

const values = getValuesToBeTransformed();

expect(values).to.include("john");
expect(values).to.include("johndoe");
expect(values).to.not.include("java");
expect(values).to.not.include("user");
expect(values).to.not.include("ab");
expect(values).to.not.include("vscode");

stubbed.restore();
});
});
2 changes: 2 additions & 0 deletions vscode/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ export function isError(obj: unknown): obj is Error {
return obj instanceof Error;
}

export const isObject = (value: any) => value !== null && typeof value === 'object' && !Array.isArray(value);

export async function initializeRunConfiguration(): Promise<boolean> {
if (vscode.workspace.name || vscode.workspace.workspaceFile) {
const java = await vscode.workspace.findFiles('**/*.java', '**/node_modules/**', 1);
Expand Down
Loading