Skip to content
Merged
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
825 changes: 546 additions & 279 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "kdb",
"description": "IDE support for kdb product suite including the q programming language",
"publisher": "KX",
"version": "1.17.0",
"version": "1.17.1",
"icon": "resources/images/kx-logo-vs.png",
"repository": {
"type": "git",
Expand Down Expand Up @@ -1218,7 +1218,7 @@
"esbuild-base": "rimraf out && node ./esbuild.mjs",
"watch": "npm run -S esbuild-base -- --sourcemap --watch",
"build": "npm run -S esbuild-base -- --sourcemap",
"vscode:prepublish": "npm run -S esbuild-base -- --minify --keep-names",
"vscode:prepublish": "npm run -S esbuild-base -- --minify",
"package": "vsce package",
"publish": "vsce publish",
"pretest": "rimraf out-test && tsc --outdir out-test --strict false",
Expand All @@ -1243,11 +1243,11 @@
"@types/sinon": "21.0.0",
"@types/vscode": "1.101.0",
"@types/vscode-webview": "1.57.5",
"@vscode/extension-telemetry": "1.0.0",
"@vscode/extension-telemetry": "^1.4.0",
"@vscode/python-extension": "1.0.6",
"@vscode/test-electron": "2.5.2",
"@vscode/vsce": "3.7.1",
"axios": "1.13.2",
"axios": "1.13.4",
"c8": "10.1.3",
"chevrotain": "10.5.0",
"cross-env": "10.1.0",
Expand All @@ -1260,7 +1260,7 @@
"extract-zip": "2.0.1",
"fs-extra": "11.3.3",
"glob": "13.0.0",
"jsdom": "27.2.0",
"jsdom": "27.4.0",
"jwt-decode": "4.0.0",
"kill-sync": "1.0.3",
"lit": "3.3.2",
Expand All @@ -1272,7 +1272,7 @@
"moment-duration-format": "2.3.2",
"moment-timezone": "0.6.0",
"node-q": "2.7.0",
"npm-check-updates": "19.3.1",
"npm-check-updates": "19.3.2",
"pick-port": "2.2.0",
"prettier": "3.6.2",
"rimraf": "6.1.2",
Expand All @@ -1285,5 +1285,10 @@
"vscode-languageserver": "9.0.1",
"vscode-languageserver-textdocument": "1.0.12",
"vscode-uri": "3.1.0"
},
"overrides": {
"chevrotain": {
"lodash": ">=4.17.23"
}
}
}
16 changes: 14 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ import { getIconPath } from "./utils/iconsUtils";
import { MessageKind, notify, Runner } from "./utils/notifications";
import { showRegistrationNotification } from "./utils/registration";
import AuthSettings from "./utils/secretStorage";
import { Telemetry } from "./utils/telemetryClient";
import { ExtensionTelemetry } from "./utils/telemetryClient";
import { addWorkspaceFile, openWith, setUriContent } from "./utils/workspace";

const logger = "extension";
Expand All @@ -128,6 +128,18 @@ export async function activate(context: vscode.ExtensionContext) {
ext.outputChannel = vscode.window.createOutputChannel("kdb", { log: true });
ext.openSslVersion = await checkOpenSslInstalled();

const extensionPackage = context.extension.packageJSON;
const extensionVersion = extensionPackage.version || "unknown";
const aiConnString = extensionPackage.aiConnString;

const isRCExtension = extensionVersion.includes("rc");
const enableTelemetry = isRCExtension
? false
: vscode.workspace.getConfiguration("telemetry").get("telemetryLevel") !==
"off";

ext.telemetry = new ExtensionTelemetry(aiConnString, enableTelemetry);

getWorkspaceLabelsConnMap();
getWorkspaceLabels();

Expand Down Expand Up @@ -1037,7 +1049,7 @@ function registerAllExtensionCommands(): void {
}

export async function deactivate(): Promise<void> {
await Telemetry.dispose();
await ext.telemetry.dispose();

if (ext.client) {
return ext.client.stop();
Expand Down
10 changes: 2 additions & 8 deletions src/extensionVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import {
ExtensionContext,
extensions,
languages,
LogOutputChannel,
OutputChannel,
Expand Down Expand Up @@ -41,12 +40,10 @@ import { QueryHistoryProvider } from "./services/queryHistoryProvider";
import { KdbResultsViewProvider } from "./services/resultsPanelProvider";
import { WorkspaceTreeProvider } from "./services/workspaceTreeProvider";
import AuthSettings from "./utils/secretStorage";
import { ExtensionTelemetry } from "./utils/telemetryClient";

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ext {
export const EXTENSION_VERSION =
extensions.getExtension("KX.kdb")?.packageJSON.version || "unknown";
export const isRCExtension = EXTENSION_VERSION.includes("rc");
export const REPL = "REPL";
export let activeTextEditor: TextEditor | undefined;
export let context: ExtensionContext;
Expand All @@ -68,6 +65,7 @@ export namespace ext {
export let openSslVersion: string | null;
export let resultPanelCSV: string;
export let isDatasourceExecution: boolean;
export let telemetry: ExtensionTelemetry;
export const rowLimit = 150000000;

export let activeConnection: LocalConnection | InsightsConnection | undefined;
Expand Down Expand Up @@ -136,10 +134,6 @@ export namespace ext {

export let client: LanguageClient;

const extensionId = "kx.kdb";
const packageJSON = extensions.getExtension(extensionId)!.packageJSON;
export const extAIConnString = packageJSON.aiConnString;

export const localhost = "127.0.0.1";
export const networkProtocols = {
http: "http://",
Expand Down
10 changes: 9 additions & 1 deletion src/utils/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,16 @@ function loadEnvironment(folder: string, env: { [key: string]: string }) {
const [key, value] = trimmed.split("=");
if (key && value !== undefined) {
env[key.trim()] = value
// Normalize
.replace(/["']/gs, "")
.replace(/(?:\$HOME|~)/gs, homedir())
// Variable expansion
.replace(
/\$([A-Za-z_]+[A-Za-z0-9_]*)|\${([A-Za-z0-9_]*)}|%([A-Za-z_]+[A-Za-z0-9_]*)%/g,
(match, a, b, c) => {
const v = a ?? b ?? c;
return env[v] ?? process.env[v] ?? match;
},
)
.trim();
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/utils/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import * as vscode from "vscode";
import { ext } from "../extensionVariables";
import { kdbOutputLog } from "./loggers";
import { stripUnprintableChars } from "./shared";
import { Telemetry } from "./telemetryClient";

const logger = "notifications";

Expand Down Expand Up @@ -132,11 +131,11 @@ export function notify<T extends string>(

if (options.telemetry) {
if (typeof options.telemetry === "boolean") {
Telemetry.sendError(new Error(message));
ext.telemetry.sendError(new Error(message));
} else if (options.telemetry instanceof Error) {
Telemetry.sendError(options.telemetry);
ext.telemetry.sendError(options.telemetry);
} else {
Telemetry.sendEvent(
ext.telemetry.sendEvent(
options.telemetry,
options.properties,
options.measurements,
Expand Down
36 changes: 5 additions & 31 deletions src/utils/telemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,23 @@
*/

import { TelemetryReporter } from "@vscode/extension-telemetry";
import * as crypto from "crypto";
import * as os from "os";
import { OutputChannel, window, workspace } from "vscode";
import { OutputChannel, window } from "vscode";

import { ext } from "../extensionVariables";

class ExtensionTelemetry {
export class ExtensionTelemetry {
private readonly output?: OutputChannel;
private readonly reporter?: TelemetryReporter;

private readonly defaultProperties: { [key: string]: any } = {};

constructor() {
const isEnableTelemetry = ext.isRCExtension
? false
: (workspace.getConfiguration("telemetry").get("enableTelemetry") ??
true);
constructor(aiConnString: string, enableTelemetry = true) {
const isTestRun = process.env.CODE_TEST || false;

if (isEnableTelemetry) {
if (enableTelemetry) {
if (isTestRun) {
this.output = window.createOutputChannel("telemetry-client-test");
} else {
try {
this.reporter = new TelemetryReporter(ext.extAIConnString);
this.defaultProperties["common.vscodemachineid"] =
generateMachineId();
this.defaultProperties["common.vscodesessionid"] =
generateSessionId();
this.reporter = new TelemetryReporter(aiConnString);
} catch (error) {
console.log(error);
}
Expand Down Expand Up @@ -88,10 +76,6 @@ class ExtensionTelemetry {
}
}

public obfuscate(data: string): string {
return crypto.createHash("sha256").update(data).digest("base64");
}

public async dispose(): Promise<void> {
if (this.reporter) {
await this.reporter.dispose();
Expand All @@ -102,13 +86,3 @@ class ExtensionTelemetry {
}
}
}

function generateMachineId(): string {
return crypto.createHash("sha256").update(os.hostname()).digest("base64");
}

function generateSessionId(): string {
return crypto.randomBytes(16).toString("hex");
}

export const Telemetry = new ExtensionTelemetry();
4 changes: 4 additions & 0 deletions test/suite/commands/serverCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ describe("serverCommand", () => {
validationHostnameStub,
validationPortStub: sinon.SinonStub;
beforeEach(() => {
ext.telemetry = {
sendEvent: sinon.stub(),
} as any;

kdbData = {
serverName: "testServer",
serverAlias: "testServerAlias",
Expand Down
6 changes: 6 additions & 0 deletions test/suite/utils/queryUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,12 @@ describe("queryUtils", () => {
});

describe("notifyExecution", () => {
beforeEach(() => {
ext.telemetry = {
sendEvent: sinon.stub(),
} as any;
});

describe("repl", () => {
describe("File", () => {
it("should return telemetry for q", () => {
Expand Down
102 changes: 102 additions & 0 deletions test/suite/utils/telemetryClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 1998-2026 KX Systems Inc.
*
* Licensed 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 * as telemetryModule from "@vscode/extension-telemetry";
import * as sinon from "sinon";
import * as vscode from "vscode";

import { ExtensionTelemetry } from "../../../src/utils/telemetryClient";

describe("ExtensionTelemetry", () => {
let telemetryReporterMock: sinon.SinonStubbedInstance<telemetryModule.TelemetryReporter>;
let outputChannelMock: any;

beforeEach(() => {
telemetryReporterMock = sinon.createStubInstance(
telemetryModule.TelemetryReporter,
);
sinon
.stub(telemetryModule, "TelemetryReporter")
.returns(telemetryReporterMock as any);

outputChannelMock = {
appendLine: sinon.spy(),
dispose: sinon.stub().resolves(),
};
sinon.stub(vscode.window, "createOutputChannel").returns(outputChannelMock);
});

afterEach(() => {
sinon.restore();
delete process.env.CODE_TEST;
});

it("should verify sendTelemetryEvent was called correctly", () => {
process.env.CODE_TEST = ""; // Production mode
const ext = new ExtensionTelemetry("conn-string", true);
const props = { category: "testing" };
const measurements = { duration: 100 };

ext.sendEvent("testEvent", props, measurements);

sinon.assert.calledOnce(telemetryReporterMock.sendTelemetryEvent);
sinon.assert.calledWith(
telemetryReporterMock.sendTelemetryEvent,
"testEvent",
sinon.match(props),
sinon.match(measurements),
);
});

it("should verify sendTelemetryErrorEvent was called correctly", () => {
process.env.CODE_TEST = "";
const ext = new ExtensionTelemetry("conn-string", true);
const testError = new Error("critical failure");
testError.name = "DatabaseError";

ext.sendError(testError, { userRole: "admin" });

sinon.assert.calledOnce(telemetryReporterMock.sendTelemetryErrorEvent);
sinon.assert.calledWith(
telemetryReporterMock.sendTelemetryErrorEvent,
"DatabaseError",
sinon.match({
name: "DatabaseError",
message: "critical failure",
userRole: "admin",
}),
);
});

it("should verify output channel logging in test mode", () => {
process.env.CODE_TEST = "true"; // Test mode
const ext = new ExtensionTelemetry("conn-string", true);

ext.sendEvent("clickAction", { button: "left" });

sinon.assert.calledOnce(outputChannelMock.appendLine);
sinon.assert.calledWithMatch(
outputChannelMock.appendLine,
/telemetry\/clickAction/,
);
});

it("should verify nothing is called when telemetry is disabled", () => {
const ext = new ExtensionTelemetry("conn-string", false);

ext.sendEvent("unusedEvent");

sinon.assert.notCalled(telemetryReporterMock.sendTelemetryEvent);
sinon.assert.notCalled(outputChannelMock.appendLine);
});
});