From 2cbafa3df6f6dd2d27113d7ee21d1ae7fe738c97 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Fri, 22 May 2026 16:15:48 +0200 Subject: [PATCH] feat: pass telemetry project id to semantic daemon --- .../daemon/semantic-layer-compute.test.ts | 6 +++- .../context/daemon/semantic-layer-compute.ts | 3 ++ packages/cli/src/managed-python-command.ts | 6 ++++ packages/cli/src/sl.test.ts | 1 + packages/cli/src/sl.ts | 2 ++ packages/cli/src/telemetry/identity.test.ts | 36 +++++++++++++++++++ packages/cli/src/telemetry/identity.ts | 18 ++++++++++ 7 files changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/context/daemon/semantic-layer-compute.test.ts b/packages/cli/src/context/daemon/semantic-layer-compute.test.ts index 846f9355..dac37ef4 100644 --- a/packages/cli/src/context/daemon/semantic-layer-compute.test.ts +++ b/packages/cli/src/context/daemon/semantic-layer-compute.test.ts @@ -106,7 +106,10 @@ describe('createPythonSemanticLayerComputePort', () => { columns: [{ name: 'orders.order_count' }], plan: { sources_used: ['orders'] }, })); - const port = createPythonSemanticLayerComputePort({ runJson }); + const port = createPythonSemanticLayerComputePort({ + runJson, + projectId: 'hashed-project-id', + }); await expect( port.query({ @@ -125,6 +128,7 @@ describe('createPythonSemanticLayerComputePort', () => { sources: [source], dialect: 'postgres', query: { measures: ['orders.order_count'], dimensions: [] }, + projectId: 'hashed-project-id', }); }); diff --git a/packages/cli/src/context/daemon/semantic-layer-compute.ts b/packages/cli/src/context/daemon/semantic-layer-compute.ts index f416b169..c590c3fa 100644 --- a/packages/cli/src/context/daemon/semantic-layer-compute.ts +++ b/packages/cli/src/context/daemon/semantic-layer-compute.ts @@ -90,6 +90,7 @@ export interface PythonSemanticLayerComputeOptions { cwd?: string; env?: NodeJS.ProcessEnv; runJson?: KtxDaemonJsonRunner; + projectId?: string; } /** @internal */ @@ -238,6 +239,7 @@ export function createPythonSemanticLayerComputePort( const command = options.command ?? 'python'; const args = options.args ?? ['-m', 'ktx_daemon']; const runJson = options.runJson ?? runProcessJson({ command, args, cwd: options.cwd, env: options.env }); + const projectId = options.projectId; return { async query(input) { @@ -245,6 +247,7 @@ export function createPythonSemanticLayerComputePort( sources: input.sources, dialect: input.dialect, query: input.query, + ...(projectId ? { projectId } : {}), }); return { sql: typeof raw.sql === 'string' ? raw.sql : '', diff --git a/packages/cli/src/managed-python-command.ts b/packages/cli/src/managed-python-command.ts index ecad702f..37cda4f3 100644 --- a/packages/cli/src/managed-python-command.ts +++ b/packages/cli/src/managed-python-command.ts @@ -12,6 +12,7 @@ import { type ManagedPythonRuntimeLayoutOptions, type ManagedPythonRuntimeStatus, } from './managed-python-runtime.js'; +import { readExistingTelemetryProjectId } from './telemetry/identity.js'; export type KtxManagedPythonInstallPolicy = 'prompt' | 'auto' | 'never'; @@ -49,6 +50,7 @@ export interface ManagedPythonCommandOptions extends ManagedPythonCommandDeps { export interface ManagedPythonSemanticLayerComputeOptions extends ManagedPythonCommandOptions { createPythonCompute?: typeof createPythonSemanticLayerComputePort; + projectDir?: string; } /** @internal */ @@ -133,8 +135,12 @@ export async function createManagedPythonSemanticLayerComputePort( ...(options.spinner ? { spinner: options.spinner } : {}), }); const createPythonCompute = options.createPythonCompute ?? createPythonSemanticLayerComputePort; + const projectId = options.projectDir + ? await readExistingTelemetryProjectId({ projectDir: options.projectDir }) + : undefined; return createPythonCompute({ command: runtime.manifest.python.daemonExecutable, args: [], + ...(projectId ? { projectId } : {}), }); } diff --git a/packages/cli/src/sl.test.ts b/packages/cli/src/sl.test.ts index 8a422025..7fa855d0 100644 --- a/packages/cli/src/sl.test.ts +++ b/packages/cli/src/sl.test.ts @@ -452,6 +452,7 @@ joins: [] cliVersion: '0.2.0', installPolicy: 'auto', io: { stdout, stderr }, + projectDir, }); expect(stdout.write).toHaveBeenCalledWith('select count(*) as order_count from public.orders\n'); }); diff --git a/packages/cli/src/sl.ts b/packages/cli/src/sl.ts index 535bf7dc..76e1092a 100644 --- a/packages/cli/src/sl.ts +++ b/packages/cli/src/sl.ts @@ -70,6 +70,7 @@ interface KtxSlDeps { cliVersion: string; installPolicy: KtxManagedPythonInstallPolicy; io: KtxSlIo; + projectDir?: string; }) => Promise; createQueryExecutor?: () => KtxSqlQueryExecutorPort; } @@ -277,6 +278,7 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx cliVersion: args.cliVersion, installPolicy: args.runtimeInstallPolicy, io, + projectDir: args.projectDir, }); const queryExecutor = args.execute ? (deps.createQueryExecutor ?? createDefaultLocalQueryExecutor)() : undefined; const result = await compileLocalSlQuery(project as KtxLocalProject, { diff --git a/packages/cli/src/telemetry/identity.test.ts b/packages/cli/src/telemetry/identity.test.ts index 15dfeae3..14722a22 100644 --- a/packages/cli/src/telemetry/identity.test.ts +++ b/packages/cli/src/telemetry/identity.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { computeTelemetryProjectId, loadTelemetryIdentity, + readExistingTelemetryProjectId, TELEMETRY_NOTICE, type TelemetryIdentityEnv, } from './identity.js'; @@ -156,4 +157,39 @@ describe('telemetry identity', () => { expect(computeTelemetryProjectId('00000000-0000-4000-8000-000000000000', projectDir)).toBe(projectId); expect(computeTelemetryProjectId('11111111-1111-4111-8111-111111111111', projectDir)).not.toBe(projectId); }); + + it('reads an existing project id for Python telemetry without creating identity', async () => { + await mkdir(join(homeDir, '.ktx'), { recursive: true }); + await writeFile( + join(homeDir, '.ktx', 'telemetry.json'), + JSON.stringify( + { + installId: '00000000-0000-4000-8000-000000000000', + enabled: true, + noticeShownAt: '2026-05-22T14:33:02.000Z', + noticeShownVersion: 1, + createdAt: '2026-05-22T14:33:02.000Z', + }, + null, + 2, + ) + '\n', + 'utf-8', + ); + + await expect( + readExistingTelemetryProjectId({ + homeDir, + projectDir: '/tmp/acme-private-project', + env: {}, + }), + ).resolves.toMatch(/^[a-f0-9]{64}$/); + + await expect( + readExistingTelemetryProjectId({ + homeDir, + projectDir: '/tmp/acme-private-project', + env: { KTX_TELEMETRY_DISABLED: '1' }, + }), + ).resolves.toBeUndefined(); + }); }); diff --git a/packages/cli/src/telemetry/identity.ts b/packages/cli/src/telemetry/identity.ts index 1d6c2fcf..069224c5 100644 --- a/packages/cli/src/telemetry/identity.ts +++ b/packages/cli/src/telemetry/identity.ts @@ -124,3 +124,21 @@ export async function loadTelemetryIdentity(options: LoadTelemetryIdentityOption export function computeTelemetryProjectId(installId: string, projectDir: string): string { return createHash('sha256').update(`${installId}:${resolve(projectDir)}`).digest('hex'); } + +export async function readExistingTelemetryProjectId(options: { + projectDir: string; + homeDir?: string; + env?: Pick; +}): Promise { + const env = options.env ?? process.env; + if (env.KTX_TELEMETRY_DISABLED || env.DO_NOT_TRACK) { + return undefined; + } + + const existing = await readTelemetryFile(telemetryPath(options.homeDir ?? homedir())); + if (!existing?.enabled) { + return undefined; + } + + return computeTelemetryProjectId(existing.installId, options.projectDir); +}