summaryrefslogtreecommitdiff
path: root/scripts/telemetry_utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/telemetry_utils.js')
-rw-r--r--scripts/telemetry_utils.js362
1 files changed, 362 insertions, 0 deletions
diff --git a/scripts/telemetry_utils.js b/scripts/telemetry_utils.js
new file mode 100644
index 00000000..62eb910b
--- /dev/null
+++ b/scripts/telemetry_utils.js
@@ -0,0 +1,362 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+import fs from 'fs';
+import net from 'net';
+import os from 'os';
+import { execSync } from 'child_process'; // Removed spawn, it's not used here
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export const ROOT_DIR = path.resolve(__dirname, '..');
+export const GEMINI_DIR = path.join(ROOT_DIR, '.gemini');
+export const OTEL_DIR = path.join(GEMINI_DIR, 'otel');
+export const BIN_DIR = path.join(OTEL_DIR, 'bin');
+export const WORKSPACE_SETTINGS_FILE = path.join(GEMINI_DIR, 'settings.json');
+
+export function getJson(url) {
+ const tmpFile = path.join(
+ os.tmpdir(),
+ `gemini-cli-releases-${Date.now()}.json`,
+ );
+ try {
+ execSync(
+ `curl -sL -H "User-Agent: gemini-cli-dev-script" -o "${tmpFile}" "${url}"`,
+ { stdio: 'pipe' },
+ );
+ const content = fs.readFileSync(tmpFile, 'utf-8');
+ return JSON.parse(content);
+ } catch (e) {
+ console.error(`Failed to fetch or parse JSON from ${url}`);
+ throw e;
+ } finally {
+ if (fs.existsSync(tmpFile)) {
+ fs.unlinkSync(tmpFile);
+ }
+ }
+}
+
+export function downloadFile(url, dest) {
+ try {
+ execSync(`curl -fL -sS -o "${dest}" "${url}"`, {
+ stdio: 'pipe',
+ });
+ return dest;
+ } catch (e) {
+ console.error(`Failed to download file from ${url}`);
+ throw e;
+ }
+}
+
+export function findFile(startPath, filter) {
+ if (!fs.existsSync(startPath)) {
+ return null;
+ }
+ const files = fs.readdirSync(startPath);
+ for (const file of files) {
+ const filename = path.join(startPath, file);
+ const stat = fs.lstatSync(filename);
+ if (stat.isDirectory()) {
+ const result = findFile(filename, filter);
+ if (result) return result;
+ } else if (filter(file)) {
+ return filename;
+ }
+ }
+ return null;
+}
+
+export function fileExists(filePath) {
+ return fs.existsSync(filePath);
+}
+
+export function readJsonFile(filePath) {
+ if (!fileExists(filePath)) {
+ return {};
+ }
+ const content = fs.readFileSync(filePath, 'utf-8');
+ try {
+ return JSON.parse(content);
+ } catch (e) {
+ console.error(`Error parsing JSON from ${filePath}: ${e.message}`);
+ return {};
+ }
+}
+
+export function writeJsonFile(filePath, data) {
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
+}
+
+export function waitForPort(port, timeout = 10000) {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now();
+ const tryConnect = () => {
+ const socket = new net.Socket();
+ socket.once('connect', () => {
+ socket.end();
+ resolve();
+ });
+ socket.once('error', (_) => {
+ if (Date.now() - startTime > timeout) {
+ reject(new Error(`Timeout waiting for port ${port} to open.`));
+ } else {
+ setTimeout(tryConnect, 500);
+ }
+ });
+ socket.connect(port, 'localhost');
+ };
+ tryConnect();
+ });
+}
+
+export async function ensureBinary(
+ executableName,
+ repo,
+ assetNameCallback,
+ binaryNameInArchive,
+ isJaeger = false,
+) {
+ const executablePath = path.join(BIN_DIR, executableName);
+ if (fileExists(executablePath)) {
+ console.log(`āœ… ${executableName} already exists at ${executablePath}`);
+ return executablePath;
+ }
+
+ console.log(`šŸ” ${executableName} not found. Downloading from ${repo}...`);
+
+ const platform = process.platform === 'win32' ? 'windows' : process.platform;
+ const arch = process.arch === 'x64' ? 'amd64' : process.arch;
+ const ext = platform === 'windows' ? 'zip' : 'tar.gz';
+
+ if (isJaeger && platform === 'windows' && arch === 'arm64') {
+ console.warn(
+ `āš ļø Jaeger does not have a release for Windows on ARM64. Skipping.`,
+ );
+ return null;
+ }
+
+ let release;
+ let asset;
+
+ if (isJaeger) {
+ console.log(`šŸ” Finding latest Jaeger v2+ asset...`);
+ const releases = getJson(`https://api.github.com/repos/${repo}/releases`);
+ const sortedReleases = releases
+ .filter((r) => !r.prerelease && r.tag_name.startsWith('v'))
+ .sort((a, b) => {
+ const aVersion = a.tag_name.substring(1).split('.').map(Number);
+ const bVersion = b.tag_name.substring(1).split('.').map(Number);
+ for (let i = 0; i < Math.max(aVersion.length, bVersion.length); i++) {
+ if ((aVersion[i] || 0) > (bVersion[i] || 0)) return -1;
+ if ((aVersion[i] || 0) < (bVersion[i] || 0)) return 1;
+ }
+ return 0;
+ });
+
+ for (const r of sortedReleases) {
+ const expectedSuffix =
+ platform === 'windows'
+ ? `-${platform}-${arch}.zip`
+ : `-${platform}-${arch}.tar.gz`;
+ const foundAsset = r.assets.find(
+ (a) =>
+ a.name.startsWith('jaeger-2.') && a.name.endsWith(expectedSuffix),
+ );
+
+ if (foundAsset) {
+ release = r;
+ asset = foundAsset;
+ console.log(
+ `ā¬‡ļø Found ${asset.name} in release ${r.tag_name}, downloading...`,
+ );
+ break;
+ }
+ }
+ if (!asset) {
+ throw new Error(
+ `Could not find a suitable Jaeger v2 asset for platform ${platform}/${arch}.`,
+ );
+ }
+ } else {
+ release = getJson(`https://api.github.com/repos/${repo}/releases/latest`);
+ const version = release.tag_name.startsWith('v')
+ ? release.tag_name.substring(1)
+ : release.tag_name;
+ const assetName = assetNameCallback(version, platform, arch, ext);
+ asset = release.assets.find((a) => a.name === assetName);
+ if (!asset) {
+ throw new Error(
+ `Could not find a suitable asset for ${repo} (version ${version}) on platform ${platform}/${arch}. Searched for: ${assetName}`,
+ );
+ }
+ }
+
+ const downloadUrl = asset.browser_download_url;
+ const tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'gemini-cli-telemetry-'),
+ );
+ const archivePath = path.join(tmpDir, asset.name);
+
+ try {
+ console.log(`ā¬‡ļø Downloading ${asset.name}...`);
+ downloadFile(downloadUrl, archivePath);
+ console.log(`šŸ“¦ Extracting ${asset.name}...`);
+
+ const actualExt = asset.name.endsWith('.zip') ? 'zip' : 'tar.gz';
+
+ if (actualExt === 'zip') {
+ execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`, { stdio: 'pipe' });
+ } else {
+ execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' });
+ }
+
+ const nameToFind = binaryNameInArchive || executableName;
+ const foundBinaryPath = findFile(tmpDir, (file) => {
+ if (platform === 'windows') {
+ return file === `${nameToFind}.exe`;
+ }
+ return file === nameToFind;
+ });
+
+ if (!foundBinaryPath) {
+ throw new Error(
+ `Could not find binary "${nameToFind}" in extracted archive at ${tmpDir}. Contents: ${fs.readdirSync(tmpDir).join(', ')}`,
+ );
+ }
+
+ fs.renameSync(foundBinaryPath, executablePath);
+
+ if (platform !== 'windows') {
+ fs.chmodSync(executablePath, '755');
+ }
+
+ console.log(`āœ… ${executableName} installed at ${executablePath}`);
+ return executablePath;
+ } finally {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ if (fs.existsSync(archivePath)) {
+ fs.unlinkSync(archivePath);
+ }
+ }
+}
+
+export function manageTelemetrySettings(
+ enable,
+ oTelEndpoint = 'http://localhost:4317',
+ originalSandboxSettingToRestore,
+) {
+ const workspaceSettings = readJsonFile(WORKSPACE_SETTINGS_FILE);
+ const currentSandboxSetting = workspaceSettings.sandbox;
+ let settingsModified = false;
+
+ if (enable) {
+ if (workspaceSettings.telemetry !== true) {
+ workspaceSettings.telemetry = true;
+ settingsModified = true;
+ console.log('āš™ļø Enabled telemetry in workspace settings.');
+ }
+ if (workspaceSettings.sandbox !== false) {
+ workspaceSettings.sandbox = false;
+ settingsModified = true;
+ console.log('āœ… Disabled sandbox mode for telemetry.');
+ }
+ if (workspaceSettings.telemetryOtlpEndpoint !== oTelEndpoint) {
+ workspaceSettings.telemetryOtlpEndpoint = oTelEndpoint;
+ settingsModified = true;
+ console.log(`šŸ”§ Set telemetry OTLP endpoint to ${oTelEndpoint}.`);
+ }
+ } else {
+ if (workspaceSettings.telemetry === true) {
+ delete workspaceSettings.telemetry;
+ settingsModified = true;
+ console.log('āš™ļø Disabled telemetry in workspace settings.');
+ }
+ if (workspaceSettings.telemetryOtlpEndpoint) {
+ delete workspaceSettings.telemetryOtlpEndpoint;
+ settingsModified = true;
+ console.log('šŸ”§ Cleared telemetry OTLP endpoint.');
+ }
+ if (
+ originalSandboxSettingToRestore !== undefined &&
+ workspaceSettings.sandbox !== originalSandboxSettingToRestore
+ ) {
+ workspaceSettings.sandbox = originalSandboxSettingToRestore;
+ settingsModified = true;
+ console.log('āœ… Restored original sandbox setting.');
+ }
+ }
+
+ if (settingsModified) {
+ writeJsonFile(WORKSPACE_SETTINGS_FILE, workspaceSettings);
+ console.log('āœ… Workspace settings updated.');
+ } else {
+ console.log(
+ enable
+ ? 'āœ… Workspace settings are already configured for telemetry.'
+ : 'āœ… Workspace settings already reflect telemetry disabled.',
+ );
+ }
+ return currentSandboxSetting;
+}
+
+export function registerCleanup(
+ getProcesses,
+ getLogFileDescriptors,
+ originalSandboxSetting,
+) {
+ let cleanedUp = false;
+ const cleanup = () => {
+ if (cleanedUp) return;
+ cleanedUp = true;
+
+ console.log('\nšŸ‘‹ Shutting down...');
+
+ manageTelemetrySettings(false, null, originalSandboxSetting);
+
+ const processes = getProcesses ? getProcesses() : [];
+ processes.forEach((proc) => {
+ if (proc && proc.pid) {
+ const name = path.basename(proc.spawnfile);
+ try {
+ console.log(`šŸ›‘ Stopping ${name} (PID: ${proc.pid})...`);
+ process.kill(proc.pid, 'SIGTERM');
+ console.log(`āœ… ${name} stopped.`);
+ } catch (e) {
+ if (e.code !== 'ESRCH') {
+ console.error(`Error stopping ${name}: ${e.message}`);
+ }
+ }
+ }
+ });
+
+ const logFileDescriptors = getLogFileDescriptors
+ ? getLogFileDescriptors()
+ : [];
+ logFileDescriptors.forEach((fd) => {
+ if (fd) {
+ try {
+ fs.closeSync(fd);
+ } catch (_) {
+ /* no-op */
+ }
+ }
+ });
+ };
+
+ process.on('exit', cleanup);
+ process.on('SIGINT', () => process.exit(0));
+ process.on('SIGTERM', () => process.exit(0));
+ process.on('uncaughtException', (err) => {
+ console.error('Uncaught Exception:', err);
+ cleanup();
+ process.exit(1);
+ });
+}