feat: pass telemetry project id to semantic daemon

This commit is contained in:
Andrey Avtomonov 2026-05-22 16:15:48 +02:00
parent 1c6c6c853f
commit 2cbafa3df6
7 changed files with 71 additions and 1 deletions

View file

@ -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',
});
});

View file

@ -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 : '',

View file

@ -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 } : {}),
});
}

View file

@ -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');
});

View file

@ -70,6 +70,7 @@ interface KtxSlDeps {
cliVersion: string;
installPolicy: KtxManagedPythonInstallPolicy;
io: KtxSlIo;
projectDir?: string;
}) => Promise<KtxSemanticLayerComputePort>;
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, {

View file

@ -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();
});
});

View file

@ -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<TelemetryIdentityEnv, 'KTX_TELEMETRY_DISABLED' | 'DO_NOT_TRACK'>;
}): Promise<string | undefined> {
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);
}