diff --git a/bundled/tool/check_consent.py b/bundled/tool/check_consent.py new file mode 100644 index 0000000..2922602 --- /dev/null +++ b/bundled/tool/check_consent.py @@ -0,0 +1,39 @@ +from common import update_sys_path +from pathlib import Path +import os +import sys +import json + +update_sys_path( + os.fspath(Path(__file__).parent.parent / "libs"), + os.getenv("LS_IMPORT_STRATEGY", "useBundled"), +) + +# important to keep this after sys.path is updated +from kedro_telemetry.plugin import _check_for_telemetry_consent +from kedro_telemetry.plugin import ( + _get_project_properties, + _get_or_create_uuid, +) + +if __name__ == "__main__": + from pathlib import Path + import sys + + if len(sys.argv) > 1: + project_path = Path(sys.argv[1]) + else: + project_path = Path.cwd() + consent = _check_for_telemetry_consent(project_path) + + # Project Metadata + + user_uuid = _get_or_create_uuid() + properties = _get_project_properties(user_uuid, project_path) + # Extension will parse this message + properties["consent"] = consent + print("telemetry consent: ", end="") + # It is important to use json.dump, if the message is printed together Python + # convert it to single quote and the result is no longer valid JSON. The message + # will be parsed by the extension client. + result = json.dump(properties, sys.stdout) diff --git a/bundled/tool/install_telemetry_dependencies.py b/bundled/tool/install_telemetry_dependencies.py new file mode 100644 index 0000000..cf47b62 --- /dev/null +++ b/bundled/tool/install_telemetry_dependencies.py @@ -0,0 +1,47 @@ +import subprocess +import sys +from pathlib import Path + +def install_dependencies(extension_root_dir): + """ + Install dependencies required for the Kedro extension. + + Args: + extension_root_dir (str): The root directory of the extension. + Raises: + ImportError: If the required dependencies are not found. + """ + ... + libs_path = Path(extension_root_dir) / "bundled" / "libs" + requirements_path = Path(extension_root_dir) / "kedro-telemetry-requirements.txt" + + try: + import kedro_telemetry + from packaging.version import parse + + version = parse(kedro_telemetry.__version__) + if version.major<1 and version.minor<6: # at least >0.6.0 + raise ImportError("kedro-telemetry version must be >=0.6.0") + except ImportError: + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-r", + Path(requirements_path), + "-t", + Path(libs_path), + "--no-cache-dir", + "--no-deps" + ] + ) + + +if __name__ == "__main__": + if len(sys.argv) > 1: + extension_root_dir = sys.argv[1] + else: + extension_root_dir = None + install_dependencies(extension_root_dir) diff --git a/bundled/tool/install_dependencies.py b/bundled/tool/install_viz_dependencies.py similarity index 99% rename from bundled/tool/install_dependencies.py rename to bundled/tool/install_viz_dependencies.py index 114d8ef..a515fcc 100644 --- a/bundled/tool/install_dependencies.py +++ b/bundled/tool/install_viz_dependencies.py @@ -18,6 +18,7 @@ def install_dependencies(extension_root_dir): try: import fastapi import orjson + except ImportError: subprocess.check_call( [ diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py index ba159ae..9969686 100644 --- a/bundled/tool/lsp_server.py +++ b/bundled/tool/lsp_server.py @@ -556,7 +556,7 @@ def definition_from_flowchart(ls, word): return result @LSP_SERVER.command("kedro.getProjectData") -def get_porject_data_from_viz(lsClient): +def get_project_data_from_viz(lsClient): """Get project data from kedro viz """ data = None diff --git a/kedro-telemetry-requirements.txt b/kedro-telemetry-requirements.txt new file mode 100644 index 0000000..497430b --- /dev/null +++ b/kedro-telemetry-requirements.txt @@ -0,0 +1,2 @@ +packaging +kedro-telemetry>=0.6.0 # First version that does not prompt for telemetry \ No newline at end of file diff --git a/kedro-viz-requirements.txt b/kedro-viz-requirements.txt index 3a5fac9..2b4f643 100644 --- a/kedro-viz-requirements.txt +++ b/kedro-viz-requirements.txt @@ -1,3 +1,3 @@ fastapi>=0.100.0,<0.200.0 pydantic>=2.0.0 # In case of FastAPI installs pydantic==1 -orjson>=3.9, <4.0 \ No newline at end of file +orjson>=3.9, <4.0 diff --git a/package-lock.json b/package-lock.json index 4f5214a..1ba61a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache2", "dependencies": { "@vscode/python-extension": "^1.0.5", + "axios": "^1.7.7", "fs-extra": "^11.2.0", "vscode-languageclient": "^8.1.0" }, @@ -1444,8 +1445,18 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/azure-devops-node-api": { "version": "12.5.0", @@ -1885,7 +1896,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2163,7 +2173,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -2783,6 +2792,26 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.8.tgz", + "integrity": "sha512-xgrmBhBToVKay1q2Tao5LI26B83UhrB/vM1avwVSDzt8rx3rO6AizBAaF46EgksTVr+rFTQaqZZ9MVBfUe4nig==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.0.tgz", @@ -2803,7 +2832,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3799,7 +3827,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -3808,7 +3835,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -4412,6 +4438,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index 6c57823..7cf840e 100644 --- a/package.json +++ b/package.json @@ -168,11 +168,11 @@ "category": "kedro", "title": "Run Kedro Viz" } - ] }, "dependencies": { "@vscode/python-extension": "^1.0.5", + "axios": "^1.7.7", "fs-extra": "^11.2.0", "vscode-languageclient": "^8.1.0" }, diff --git a/src/common/constants.ts b/src/common/constants.ts index e33f333..ffd31dd 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -9,3 +9,9 @@ export const EXTENSION_ROOT_DIR = export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled'); export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `lsp_server.py`); export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `_debug_server.py`); + + +// Global state +export const DEPENDENCIES_INSTALLED = 'kedro.dependenciesInstalled' +export const TELEMETRY_CONSENT = 'kedro.telemetryConsent'; +export const PROJECT_METADATA = 'kedro.projectMetadata'; \ No newline at end of file diff --git a/src/common/telemetry.ts b/src/common/telemetry.ts new file mode 100644 index 0000000..de80749 --- /dev/null +++ b/src/common/telemetry.ts @@ -0,0 +1,48 @@ +import axios from 'axios'; + +// from kedro_telemetry.plugin +interface HeapData { + app_id: string; + event: string; + timestamp: string; + properties: any; + identity?: string; +} + +const HEAP_APPID_PROD = '4039408868'; // todo: Dev server, change it back to prod +const HEAP_ENDPOINT = 'https://heapanalytics.com/api/track'; +const HEAP_HEADERS = { 'Content-Type': 'application/json' }; + +export async function sendHeapEvent(commandName: string, properties?: any, identity?: string): Promise { + var telemetryData; + const eventName = "Kedro VSCode Command"; + + if (properties) { + telemetryData = { ...properties }; + telemetryData["command_name"] = commandName; + } + + + const data: HeapData = { + app_id: HEAP_APPID_PROD, + event: eventName, + timestamp: new Date().toISOString(), + properties: telemetryData || {}, + }; + + if (identity) { + data.identity = identity; + } + + try { + const response = await axios.post(HEAP_ENDPOINT, data, { + headers: HEAP_HEADERS, + timeout: 10000, // 10 seconds + }); + + // Handle the response if needed + console.log('Heap event sent successfully:', response.status); + } catch (error) { + console.error('Error sending Heap event:', error); + } +} diff --git a/src/common/utilities.ts b/src/common/utilities.ts index 12d27b0..a4733e5 100644 --- a/src/common/utilities.ts +++ b/src/common/utilities.ts @@ -9,7 +9,7 @@ import { LogLevel, Uri, WorkspaceFolder } from 'vscode'; import { Trace } from 'vscode-jsonrpc/node'; import { getWorkspaceFolders } from './vscodeapi'; import { callPythonScript } from './callPythonScript'; -import { EXTENSION_ROOT_DIR } from './constants'; +import { DEPENDENCIES_INSTALLED, EXTENSION_ROOT_DIR, PROJECT_METADATA, TELEMETRY_CONSENT } from './constants'; import { traceError, traceLog } from './log/logging'; import { executeGetProjectDataCommand } from './commands'; import KedroVizPanel from '../webview/vizWebView'; @@ -74,19 +74,25 @@ export async function getProjectRoot(): Promise { } export async function installDependenciesIfNeeded(context: vscode.ExtensionContext): Promise { - const alreadyInstalled = context.globalState.get('dependenciesInstalled', false); + // Install necessary dependencies for the flowcharts and telemetry + const alreadyInstalled = context.globalState.get(DEPENDENCIES_INSTALLED, false); if (!alreadyInstalled) { - const pathToScript = 'bundled/tool/install_dependencies.py'; + const vizPathToScript = 'bundled/tool/install_viz_dependencies.py'; + const telemetryPathToScript = 'bundled/tool/install_telemetry_dependencies.py'; try { - const stdout = await callPythonScript(pathToScript, EXTENSION_ROOT_DIR, context); - + const stdoutViz = await callPythonScript(vizPathToScript, EXTENSION_ROOT_DIR, context); + const stdoutTelemetry = await callPythonScript(telemetryPathToScript, EXTENSION_ROOT_DIR, context); // Check if the script output contains the success message - if (stdout.includes('Successfully installed')) { - context.globalState.update('dependenciesInstalled', true); - traceLog(`Python dependencies installed!`); - console.log('Python dependencies installed!'); + if (stdoutViz.includes('Successfully installed')) { + traceLog(`Kedro-viz dependencies installed!`); + console.log('Kedro-viz dependencies installed!'); + } + if (stdoutTelemetry.includes('Successfully installed')) { + traceLog(`kedro-telemetry dependencies installed!`); + console.log('kedro-telemetry dependencies installed!'); } + context.globalState.update(DEPENDENCIES_INSTALLED, true); } catch (error) { traceError(`Failed to install Python dependencies:: ${error}`); console.error(`Failed to install Python dependencies:: ${error}`); @@ -94,7 +100,51 @@ export async function installDependenciesIfNeeded(context: vscode.ExtensionConte } } + +export async function checkKedroProjectConsent(context: vscode.ExtensionContext): Promise { + const pathToScript = 'bundled/tool/check_consent.py'; + try { + const stdout = await callPythonScript(pathToScript, EXTENSION_ROOT_DIR, context); + const telemetryResult = parseTelemetryConsent(stdout); + + // Check if the script output contains the success message + if (telemetryResult) { + const consent = telemetryResult['consent']; + context.globalState.update(PROJECT_METADATA, telemetryResult); + delete telemetryResult['consent']; + + context.globalState.update(TELEMETRY_CONSENT, consent); + console.log(`Consent from Kedro Project: ${consent}`); + return consent; + } + return false; + } catch (error) { + traceError(`Failed to check for telemetry consent:: ${error}`); + } + return false; +} + +function parseTelemetryConsent(logMessage: string): Record | null { + // Step 1: Define a regular expression to match the telemetry consent data + const telemetryRegex = /telemetry consent: ({.*})/; + const match = logMessage.match(telemetryRegex); + + if (match && match[1]) { + try { + const telemetryData = JSON.parse(match[1]); + return telemetryData; + } catch (error) { + console.error('Failed to parse telemetry consent data:', error); + return null; + } + } else { + console.log('Telemetry consent data not found in log message.'); + return null; + } +} + export async function updateKedroVizPanel(lsClient: LanguageClient | undefined): Promise { const projectData = await executeGetProjectDataCommand(lsClient); KedroVizPanel.currentPanel?.updateData(projectData); + } diff --git a/src/extension.ts b/src/extension.ts index 680a21c..134426b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,19 +17,24 @@ import { onDidChangePythonInterpreter, resolveInterpreter, } from './common/python'; +import { sendHeapEvent } from './common/telemetry'; import { restartServer } from './common/server'; import { checkIfConfigurationChanged, getInterpreterFromSetting } from './common/settings'; import { loadServerDefaults } from './common/setup'; import { createStatusBar } from './common/status_bar'; -import { getLSClientTraceLevel, installDependenciesIfNeeded, updateKedroVizPanel } from './common/utilities'; +import { getLSClientTraceLevel, installDependenciesIfNeeded, updateKedroVizPanel, checkKedroProjectConsent } from './common/utilities'; import { createOutputChannel, onDidChangeConfiguration, registerCommand } from './common/vscodeapi'; import KedroVizPanel from './webview/vizWebView'; +import { PROJECT_METADATA, TELEMETRY_CONSENT } from './common/constants'; let lsClient: LanguageClient | undefined; export async function activate(context: vscode.ExtensionContext): Promise { await installDependenciesIfNeeded(context); + // Check for consent in the Kedro Project + const consent = await checkKedroProjectConsent(context); + // This is required to get server name and module. This should be // the first thing that we do in this extension. const serverInfo = loadServerDefaults(); @@ -45,6 +50,7 @@ export async function activate(context: vscode.ExtensionContext): Promise const CMD_RESTART_SERVER = `${serverId}.restart`; const CMD_SELECT_ENV = `${serverId}.selectEnvironment`; const CMD_RUN_KEDRO_VIZ = `${serverId}.runKedroViz`; + const CMD_DEFINITION_REQUEST = 'kedro.sendDefinitionRequest'; // Status Bar const statusBarItem = await createStatusBar(CMD_SELECT_ENV, serverId); @@ -68,12 +74,6 @@ export async function activate(context: vscode.ExtensionContext): Promise }), ); - context.subscriptions.push( - registerCommand(CMD_RUN_KEDRO_VIZ, async () => { - KedroVizPanel.createOrShow(context.extensionUri); - updateKedroVizPanel(lsClient); - }), - ); // Log Server information traceLog(`Name: ${serverInfo.name}`); @@ -114,6 +114,17 @@ export async function activate(context: vscode.ExtensionContext): Promise ); }; + let projectMetadata: undefined; + let heapUserId: string = ''; + projectMetadata = context.globalState.get(PROJECT_METADATA); + if (projectMetadata) { + heapUserId = projectMetadata['username']; + } + + const sendHeapEventWithMetadata = async (eventName: string): Promise => { + sendHeapEvent(eventName, projectMetadata, heapUserId); + }; + context.subscriptions.push( onDidChangePythonInterpreter(async () => { await runServer(); @@ -125,6 +136,7 @@ export async function activate(context: vscode.ExtensionContext): Promise }), registerCommand(CMD_RESTART_SERVER, async () => { await runServer(); + await sendHeapEventWithMetadata(CMD_RESTART_SERVER); // If KedroVizPanel is open, update the data on server restart if (KedroVizPanel.currentPanel) { @@ -137,12 +149,20 @@ export async function activate(context: vscode.ExtensionContext): Promise if (result) { statusBarItem.text = `$(kedro-logo) base + ${result.label}`; } + await sendHeapEventWithMetadata(CMD_SELECT_ENV); }), registerCommand('pygls.server.executeCommand', async () => { await executeServerCommand(lsClient); }), - registerCommand('kedro.sendDefinitionRequest', async (word) => { + registerCommand(CMD_DEFINITION_REQUEST, async (word) => { await executeServerDefinitionCommand(lsClient, word); + await sendHeapEventWithMetadata(CMD_DEFINITION_REQUEST); + }), + registerCommand(CMD_RUN_KEDRO_VIZ, async () => { + KedroVizPanel.createOrShow(context.extensionUri); + const projectData = await executeGetProjectDataCommand(lsClient); + KedroVizPanel.currentPanel?.updateData(projectData); + await sendHeapEventWithMetadata(CMD_RUN_KEDRO_VIZ); }), );