@databricks/appkit 0.53.1 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/appkit/package.js +1 -1
  2. package/dist/cli/commands/generate-types.js +1 -1
  3. package/dist/cli/commands/generate-types.js.map +1 -1
  4. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  5. package/dist/plugins/analytics/analytics.js +7 -1
  6. package/dist/plugins/analytics/analytics.js.map +1 -1
  7. package/dist/plugins/analytics/metric.js +1 -0
  8. package/dist/plugins/analytics/mv/index.js +1 -0
  9. package/dist/plugins/analytics/mv/metadata.js +30 -0
  10. package/dist/plugins/analytics/mv/metadata.js.map +1 -0
  11. package/dist/plugins/analytics/types.d.ts +7 -0
  12. package/dist/plugins/analytics/types.d.ts.map +1 -1
  13. package/dist/plugins/analytics/types.js.map +1 -1
  14. package/dist/shared/src/index.d.ts +1 -0
  15. package/dist/shared/src/metric-metadata.d.ts +24 -0
  16. package/dist/shared/src/metric-metadata.d.ts.map +1 -0
  17. package/dist/shared/src/sse/analytics.js +2 -1
  18. package/dist/shared/src/sse/analytics.js.map +1 -1
  19. package/dist/type-generator/errors.js +0 -3
  20. package/dist/type-generator/errors.js.map +1 -1
  21. package/dist/type-generator/index.js +15 -10
  22. package/dist/type-generator/index.js.map +1 -1
  23. package/dist/type-generator/mv-registry/render-types.js +49 -11
  24. package/dist/type-generator/mv-registry/render-types.js.map +1 -1
  25. package/dist/type-generator/query-registry.js +1 -3
  26. package/dist/type-generator/query-registry.js.map +1 -1
  27. package/dist/type-generator/vite-plugin.d.ts +4 -2
  28. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  29. package/dist/type-generator/vite-plugin.js +1 -0
  30. package/dist/type-generator/vite-plugin.js.map +1 -1
  31. package/docs/development/type-generation.md +7 -7
  32. package/package.json +1 -1
  33. package/sbom.cdx.json +1 -1
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.53.1";
3
+ var version = "0.54.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -50,7 +50,7 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
50
50
  const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);
51
51
  if (fs.existsSync(metricConfig)) {
52
52
  const typesDir = path.dirname(resolvedOutFile);
53
- console.log(`Generated metric types: ${path.join(typesDir, "metric-views.d.ts")}`);
53
+ console.log(`Generated metric types: ${path.join(typesDir, "metric-views.ts")}`);
54
54
  }
55
55
  }
56
56
  } else console.error("Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.");
@@ -1 +1 @@
1
- {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport { METRIC_CONFIG_FILE } from \"../../schemas/metric-fqn\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n const metricViewsFolder = path.join(\n resolvedRootDir,\n \"config/metric-views\",\n );\n const hasQueries = fs.existsSync(queryFolder);\n const hasMetricViews = fs.existsSync(metricViewsFolder);\n\n // Generate when either config surface exists. Metric-view types are\n // independent of `.sql` queries — an app can declare metric views in\n // `config/metric-views/` without a `config/queries/` folder.\n if (hasQueries || hasMetricViews) {\n await typeGen.generateFromEntryPoint({\n queryFolder: hasQueries ? queryFolder : undefined,\n metricViewsFolder: hasMetricViews ? metricViewsFolder : undefined,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n\n if (hasQueries) {\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n\n const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.d.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport { METRIC_CONFIG_FILE } from \"../../schemas/metric-fqn\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n const metricViewsFolder = path.join(\n resolvedRootDir,\n \"config/metric-views\",\n );\n const hasQueries = fs.existsSync(queryFolder);\n const hasMetricViews = fs.existsSync(metricViewsFolder);\n\n // Generate when either config surface exists. Metric-view types are\n // independent of `.sql` queries — an app can declare metric views in\n // `config/metric-views/` without a `config/queries/` folder.\n if (hasQueries || hasMetricViews) {\n await typeGen.generateFromEntryPoint({\n queryFolder: hasQueries ? queryFolder : undefined,\n metricViewsFolder: hasMetricViews ? metricViewsFolder : undefined,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n\n if (hasQueries) {\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n\n const metricConfig = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;cA0Ga,eAAA,SAAwB,MAAA,YAAkB,YAAA;;SAE9C,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;;;AATV;;;;;;UAmBU,gBAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA2ClB;;;;;;EAHG,mBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA4RA;;;;;;;EAAA,QAvQW,mBAAA;EAm0BI;;;;;EAvyBZ,eAAA,CAAgB,WAAA,WAAsB,OAAA;EAq1BkB;;;;EA70BxD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAuwBA;;;;;;;;;;;;;;;EA1iBG,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAlViB;;;;;;EAAA,QAwkBN,qBAAA;EAphBC;;;;;;;;;EAAA,QAijBP,sBAAA;EAtfF;;;;;;;;;;;EAAA,QAshBQ,uBAAA;EApTP;;;;;;;EAycD,0BAAA,CAA2B,MAAA,EAAQ,WAAA,GAAc,OAAA;EAAd;;;;;;;;;;;;;;;;;;EAAA,QAiCjC,qBAAA;EAkHF;;;;;;;;;;;;;;;EAzDA,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EAqBG,QAAA,CAAA,GAAY,OAAA;EAAA,QAIV,KAAA;EAuBR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EA1DqC;;;;AA2J1C;;;EAtFE,OAAA,CAAQ,IAAA,GAXE,cAAA,GAWoD,MAAA,SAAA,YAAA;EAsF1C;;;;EA9EpB,OAAA,CAAA;IA8EoB;;;2BA7JL,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAyJQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
1
+ {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;cA2Ga,eAAA,SAAwB,MAAA,YAAkB,YAAA;;SAE9C,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;;;AATV;;;;;;UAmBU,gBAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA2ClB;;;;;;EAHG,mBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA4RA;;;;;;;EAAA,QAvQW,mBAAA;EAo1BI;;;;;EAxzBZ,eAAA,CAAgB,WAAA,WAAsB,OAAA;EAs2BkB;;;;EA91BxD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAwxBA;;;;;;;;;;;;;;;EA3jBG,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAlViB;;;;;;EAAA,QAylBN,qBAAA;EAriBC;;;;;;;;;EAAA,QAkkBP,sBAAA;EAvgBF;;;;;;;;;;;EAAA,QAuiBQ,uBAAA;EArUP;;;;;;;EA0dD,0BAAA,CAA2B,MAAA,EAAQ,WAAA,GAAc,OAAA;EAAd;;;;;;;;;;;;;;;;;;EAAA,QAiCjC,qBAAA;EAkHF;;;;;;;;;;;;;;;EAzDA,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EAqBG,QAAA,CAAA,GAAY,OAAA;EAAA,QAIV,KAAA;EAuBR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EA1DqC;;;;AA2J1C;;;EAtFE,OAAA,CAAQ,IAAA,GAXE,cAAA,GAWoD,MAAA,SAAA,YAAA;EAsF1C;;;;EA9EpB,OAAA,CAAA;IA8EoB;;;2BA7JL,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAyJQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
@@ -17,6 +17,7 @@ import { queryDefaults } from "./defaults.js";
17
17
  import manifest_default from "./manifest.js";
18
18
  import { buildMetricSql } from "./mv/formatters.js";
19
19
  import { composeMetricCacheKey, deriveMetricExecutorKey } from "./mv/cache.js";
20
+ import { selectMetricMetadata } from "./mv/metadata.js";
20
21
  import { loadMetricRegistry } from "./mv/registry.js";
21
22
  import { normalizeAnalyticsFormat } from "./types.js";
22
23
  import { validateMetricRequest } from "./mv/schemas.js";
@@ -336,6 +337,7 @@ var AnalyticsPlugin = class extends Plugin {
336
337
  }
337
338
  throw err;
338
339
  }
340
+ const metadata = selectMetricMetadata(this.config.metricViewsMetadata, key, request.measures, request.dimensions);
339
341
  const cacheConfig = {
340
342
  ...queryDefaults.cache,
341
343
  cacheKey: composeMetricCacheKey({
@@ -397,7 +399,11 @@ var AnalyticsPlugin = class extends Plugin {
397
399
  const inner = msg.startsWith("Statement failed: ") ? msg.slice(18) : msg;
398
400
  throw ExecutionError.statementFailed(inner);
399
401
  }
400
- yield sqlResult.data;
402
+ const resultMessage = sqlResult.data;
403
+ yield metadata !== void 0 ? {
404
+ ...resultMessage,
405
+ metadata
406
+ } : resultMessage;
401
407
  }, streamExecutionSettings, executorKey);
402
408
  }
403
409
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWarehouseId, getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricRegistry,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"analytics\">;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWarehouseId, getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricRegistry,\n selectMetricMetadata,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"analytics\">;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n // Computed here, outside the cached execute below, so a cache hit still\n // serves the current metadata. Absent config → `undefined` → the `result`\n // message omits the field (envelope-identical to `/query`).\n const metadata = selectMetricMetadata(\n this.config.metricViewsMetadata,\n key,\n request.measures,\n request.dimensions,\n );\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n // Stamp the metadata onto the (possibly cached) result message; the\n // cached message never carries it.\n const resultMessage = sqlResult.data as AnalyticsSseMessage;\n yield (\n metadata !== undefined\n ? { ...resultMessage, metadata }\n : resultMessage\n ) as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { buildMetricSql } from "./mv/formatters.js";
2
2
  import { composeMetricCacheKey, deriveMetricExecutorKey } from "./mv/cache.js";
3
+ import { selectMetricMetadata } from "./mv/metadata.js";
3
4
  import { loadMetricRegistry } from "./mv/registry.js";
4
5
  import { validateMetricRequest } from "./mv/schemas.js";
5
6
  import "./mv/index.js";
@@ -1,5 +1,6 @@
1
1
  import { buildMetricSql } from "./formatters.js";
2
2
  import { composeMetricCacheKey, deriveMetricExecutorKey } from "./cache.js";
3
+ import { selectMetricMetadata } from "./metadata.js";
3
4
  import { loadMetricRegistry } from "./registry.js";
4
5
  import { validateMetricRequest } from "./schemas.js";
5
6
 
@@ -0,0 +1,30 @@
1
+ //#region src/plugins/analytics/mv/metadata.ts
2
+ /**
3
+ * Flatten the injected {@link MetricViewsMetadata} for `key` into a single
4
+ * `Record<column, meta>` covering only the requested measures and dimensions,
5
+ * so the client can label/format just the columns it queried.
6
+ *
7
+ * Pure response decoration: it never touches the cache key or the SQL, and
8
+ * reads only from the injected value (never disk / DESCRIBE at runtime).
9
+ *
10
+ * Lookups go through {@link Object.hasOwn}, so neither an inherited metric key
11
+ * nor an inherited column name (`toString`, `__proto__`, …) can resolve to a
12
+ * bogus entry. Requested columns absent from the metadata are omitted rather
13
+ * than placeheld.
14
+ *
15
+ * Returns `undefined` rather than an empty object when there is nothing to
16
+ * stamp, so the caller can omit the field and keep the message byte-identical
17
+ * to a plain `/query` result.
18
+ */
19
+ function selectMetricMetadata(all, key, measures, dimensions) {
20
+ if (!all || !Object.hasOwn(all, key)) return;
21
+ const entry = all[key];
22
+ const slice = {};
23
+ for (const measure of measures) if (Object.hasOwn(entry.measures, measure)) slice[measure] = entry.measures[measure];
24
+ for (const dimension of dimensions ?? []) if (Object.hasOwn(entry.dimensions, dimension)) slice[dimension] = entry.dimensions[dimension];
25
+ return Object.keys(slice).length > 0 ? slice : void 0;
26
+ }
27
+
28
+ //#endregion
29
+ export { selectMetricMetadata };
30
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","names":[],"sources":["../../../../src/plugins/analytics/mv/metadata.ts"],"sourcesContent":["import type { MetricViewColumnDisplay, MetricViewsMetadata } from \"shared\";\n\n/**\n * Flatten the injected {@link MetricViewsMetadata} for `key` into a single\n * `Record<column, meta>` covering only the requested measures and dimensions,\n * so the client can label/format just the columns it queried.\n *\n * Pure response decoration: it never touches the cache key or the SQL, and\n * reads only from the injected value (never disk / DESCRIBE at runtime).\n *\n * Lookups go through {@link Object.hasOwn}, so neither an inherited metric key\n * nor an inherited column name (`toString`, `__proto__`, …) can resolve to a\n * bogus entry. Requested columns absent from the metadata are omitted rather\n * than placeheld.\n *\n * Returns `undefined` rather than an empty object when there is nothing to\n * stamp, so the caller can omit the field and keep the message byte-identical\n * to a plain `/query` result.\n */\nexport function selectMetricMetadata(\n all: MetricViewsMetadata | undefined,\n key: string,\n measures: string[],\n dimensions: string[] | undefined,\n): Record<string, MetricViewColumnDisplay> | undefined {\n if (!all || !Object.hasOwn(all, key)) {\n return undefined;\n }\n\n const entry = all[key];\n const slice: Record<string, MetricViewColumnDisplay> = {};\n\n for (const measure of measures) {\n if (Object.hasOwn(entry.measures, measure)) {\n slice[measure] = entry.measures[measure];\n }\n }\n for (const dimension of dimensions ?? []) {\n if (Object.hasOwn(entry.dimensions, dimension)) {\n slice[dimension] = entry.dimensions[dimension];\n }\n }\n\n return Object.keys(slice).length > 0 ? slice : undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,SAAgB,qBACd,KACA,KACA,UACA,YACqD;AACrD,KAAI,CAAC,OAAO,CAAC,OAAO,OAAO,KAAK,IAAI,CAClC;CAGF,MAAM,QAAQ,IAAI;CAClB,MAAM,QAAiD,EAAE;AAEzD,MAAK,MAAM,WAAW,SACpB,KAAI,OAAO,OAAO,MAAM,UAAU,QAAQ,CACxC,OAAM,WAAW,MAAM,SAAS;AAGpC,MAAK,MAAM,aAAa,cAAc,EAAE,CACtC,KAAI,OAAO,OAAO,MAAM,YAAY,UAAU,CAC5C,OAAM,aAAa,MAAM,WAAW;AAIxC,QAAO,OAAO,KAAK,MAAM,CAAC,SAAS,IAAI,QAAQ"}
@@ -1,9 +1,16 @@
1
1
  import { BasePluginConfig } from "../../shared/src/plugin.js";
2
+ import { MetricViewsMetadata } from "../../shared/src/metric-metadata.js";
2
3
  import "../../shared/src/index.js";
3
4
 
4
5
  //#region src/plugins/analytics/types.d.ts
5
6
  interface IAnalyticsConfig extends BasePluginConfig {
6
7
  timeout?: number;
8
+ /**
9
+ * Build-generated per-metric column metadata, keyed by metric key. The
10
+ * metric route scopes this to the requested measures and dimensions before
11
+ * attaching it to the SSE result.
12
+ */
13
+ metricViewsMetadata?: MetricViewsMetadata;
7
14
  /**
8
15
  * Maximum time (ms) the analytics route waits for a STOPPED/STARTING SQL
9
16
  * warehouse to reach RUNNING before failing the request. Defaults to 5 min.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/analytics/types.ts"],"mappings":";;;;UAEiB,gBAAA,SAAyB,gBAAA;EACxC,OAAA;;AADF;;;EAME,yBAAA;EANwC;;;;;;;EAcxC,kBAAA;;;;;;;;EAQA,uBAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/analytics/types.ts"],"mappings":";;;;;UAMiB,gBAAA,SAAyB,gBAAA;EACxC,OAAA;;;AADF;;;EAOE,mBAAA,GAAsB,mBAAA;EAPkB;;;;EAYxC,yBAAA;EAQA;;;;;;;EAAA,kBAAA;;;;;;;;EAQA,uBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/plugins/analytics/types.ts"],"sourcesContent":["import type { BasePluginConfig } from \"shared\";\n\nexport interface IAnalyticsConfig extends BasePluginConfig {\n timeout?: number;\n /**\n * Maximum time (ms) the analytics route waits for a STOPPED/STARTING SQL\n * warehouse to reach RUNNING before failing the request. Defaults to 5 min.\n */\n warehouseStartupTimeoutMs?: number;\n /**\n * When `true` (default), a `STOPPED` SQL warehouse is auto-started on the\n * first analytics request that reaches it. Set to `false` for cost-\n * controlled deployments where billable warehouse starts must not be\n * triggered by user requests; in that case `STOPPED` surfaces as a\n * `ConfigurationError`.\n */\n autoStartWarehouse?: boolean;\n /**\n * Fail-fast ceiling (ms) for an `ARROW_STREAM` query to produce its first\n * byte (warehouse readiness + execute + first chunk). Past this, a stuck or\n * overloaded warehouse returns a `503` (`WAREHOUSE_UNAVAILABLE`) instead of\n * hanging until the client disconnects. Defaults to 2 min. Once the first\n * byte arrives the stream is not time-bounded.\n */\n arrowFirstByteTimeoutMs?: number;\n}\n\n/**\n * SQL warehouse lifecycle states surfaced by the analytics route.\n * Mirrors the states emitted by the Databricks SQL SDK (`sql.State`).\n */\nexport type WarehouseState =\n | \"RUNNING\"\n | \"STARTING\"\n | \"STOPPED\"\n | \"STOPPING\"\n | \"DELETED\"\n | \"DELETING\";\n\n/**\n * Snapshot of warehouse readiness streamed to the client over SSE before the\n * SQL result. Lets the UI render a \"warehouse starting…\" affordance instead\n * of a frozen spinner during cold starts.\n *\n * Note: the SDK's `health.summary` is intentionally NOT forwarded here. It's\n * free-form operator-oriented diagnostic text (cluster IDs, capacity-failure\n * reasons, internal RPC errors) that must not reach end users; it stays in\n * server-side telemetry only.\n */\nexport interface WarehouseStatus {\n state: WarehouseState;\n /** Milliseconds elapsed since the route began waiting for the warehouse. */\n elapsedMs: number;\n}\n\n/**\n * Discriminated union of every SSE message shape emitted by\n * `POST /api/analytics/query/:query_key`. Useful for typing the client-side\n * `onMessage` handler (and is the source of truth re-mirrored in\n * `appkit-ui` since that package can't depend on `appkit`).\n */\nexport type AnalyticsStreamMessage =\n | { type: \"warehouse_status\"; status: WarehouseStatus }\n | { type: \"result\"; data: unknown[] }\n | {\n type: \"arrow\";\n statement_id: string;\n status: { state: string };\n }\n | { type: \"error\"; error: string; code?: string };\n\n/**\n * Supported response formats for analytics queries.\n *\n * \"JSON\" and \"ARROW\" are legacy aliases kept for backwards compatibility\n * with appkit/appkit-ui < 0.33.0 — safe to remove once no consumer is on\n * a pre-0.33.0 version. The route handler normalizes them to their\n * canonical equivalents before any downstream code reads the value.\n */\nexport type AnalyticsFormat =\n | \"JSON_ARRAY\"\n | \"ARROW_STREAM\"\n /** @deprecated Use \"JSON_ARRAY\". Safe to remove once no consumer is on appkit < 0.33.0. */\n | \"JSON\"\n /** @deprecated Use \"ARROW_STREAM\". Safe to remove once no consumer is on appkit < 0.33.0. */\n | \"ARROW\";\n\n/** Canonical (post-normalization) analytics format values. */\ntype CanonicalAnalyticsFormat = \"JSON_ARRAY\" | \"ARROW_STREAM\";\n\n/**\n * Map a (possibly legacy) AnalyticsFormat to its canonical form.\n * Legacy values come from appkit/appkit-ui < 0.33.0 and can be removed\n * along with the deprecated aliases once no such consumer remains.\n */\nexport function normalizeAnalyticsFormat(\n f: AnalyticsFormat,\n): CanonicalAnalyticsFormat {\n if (f === \"JSON\") return \"JSON_ARRAY\";\n if (f === \"ARROW\") return \"ARROW_STREAM\";\n return f;\n}\n\nexport interface IAnalyticsQueryRequest {\n parameters?: Record<string, any>;\n format?: AnalyticsFormat;\n}\n\nexport interface AnalyticsQueryResponse {\n chunk_index: number;\n row_offset: number;\n row_count: number;\n data: any[];\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Metric views — POST /api/analytics/metric/:key\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Execution lane for a registered metric view, derived from the entry's\n * `executor` in `definitions.json`:\n * - `\"sp\"` ← `executor: \"app_service_principal\"` — queried as the app\n * service principal (cache shared across all users).\n * - `\"obo\"` ← `executor: \"user\"` — queried on-behalf-of the requesting\n * user (per-user cache). OBO dispatch is wired in a later phase.\n */\nexport type MetricLane = \"sp\" | \"obo\";\n\n/**\n * A single registered metric view, loaded from `config/metric-views/definitions.json`.\n *\n * The registration carries only what the runtime needs to build and dispatch\n * SQL: the metric `key`, the three-part UC FQN `source`, and the `lane`. There\n * is intentionally NO build-time measure/dimension metadata here — the security\n * boundary is the grammar gate plus parameterized values, not a name allowlist,\n * so the runtime never enumerates known measures/dimensions.\n */\nexport interface MetricRegistration {\n key: string;\n source: string;\n lane: MetricLane;\n}\n\n/**\n * v1 filter operator vocabulary — exactly twelve names. The runtime tuple\n * `METRIC_FILTER_OPERATORS` (next to the validator in `metric.ts`) is the\n * server-side source of truth; this union mirrors it statically.\n */\nexport type MetricFilterOperatorName =\n | \"equals\"\n | \"notEquals\"\n | \"in\"\n | \"notIn\"\n | \"gt\"\n | \"gte\"\n | \"lt\"\n | \"lte\"\n | \"contains\"\n | \"notContains\"\n | \"set\"\n | \"notSet\";\n\n/**\n * A single filter predicate — the leaf node of the recursive\n * {@link MetricFilter} tree. `member` is a dimension name (grammar-gated, not\n * allowlisted); `values` is bound through parameterized `:f_<idx>` bind vars\n * and never interpolated into the SQL string.\n */\nexport interface MetricPredicate {\n member: string;\n operator: MetricFilterOperatorName;\n values?: ReadonlyArray<string | number>;\n}\n\n/**\n * Recursive filter expression for the metric-view request body: a leaf\n * {@link MetricPredicate} or an `{ and: [...] }` / `{ or: [...] }` group. The\n * shape is intentionally non-generic server-side — per-metric narrowing (if\n * any) lives client-side.\n */\nexport type MetricFilter =\n | MetricPredicate\n | { and: ReadonlyArray<MetricFilter> }\n | { or: ReadonlyArray<MetricFilter> };\n\n/**\n * Validated request body for `POST /api/analytics/metric/:key`.\n *\n * `measures` is required. `dimensions` drive `GROUP BY ALL`; `filter` is the\n * recursive structured predicate tree translated into a parameterized `WHERE`\n * clause. `timeGrain` buckets the single dimension named by `timeDimension`\n * via `date_trunc`; it requires `timeDimension`, and `timeDimension` must be\n * one of `dimensions` so it is selected and in `GROUP BY ALL`. Both tokens are\n * grammar-gated before they reach SQL.\n */\nexport interface IAnalyticsMetricRequest {\n measures: string[];\n dimensions?: string[];\n filter?: MetricFilter;\n timeGrain?: string;\n /**\n * The single dimension that `timeGrain` buckets via `date_trunc`. Must be\n * one of `dimensions` (so it is selected and in `GROUP BY ALL`) and is\n * required whenever `timeGrain` is set. Grammar-gated as a SQL identifier.\n */\n timeDimension?: string;\n limit?: number;\n format?: AnalyticsFormat;\n}\n"],"mappings":";;;;;;AA+FA,SAAgB,yBACd,GAC0B;AAC1B,KAAI,MAAM,OAAQ,QAAO;AACzB,KAAI,MAAM,QAAS,QAAO;AAC1B,QAAO"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/plugins/analytics/types.ts"],"sourcesContent":["import type {\n BasePluginConfig,\n MetricViewColumnDisplay,\n MetricViewsMetadata,\n} from \"shared\";\n\nexport interface IAnalyticsConfig extends BasePluginConfig {\n timeout?: number;\n /**\n * Build-generated per-metric column metadata, keyed by metric key. The\n * metric route scopes this to the requested measures and dimensions before\n * attaching it to the SSE result.\n */\n metricViewsMetadata?: MetricViewsMetadata;\n /**\n * Maximum time (ms) the analytics route waits for a STOPPED/STARTING SQL\n * warehouse to reach RUNNING before failing the request. Defaults to 5 min.\n */\n warehouseStartupTimeoutMs?: number;\n /**\n * When `true` (default), a `STOPPED` SQL warehouse is auto-started on the\n * first analytics request that reaches it. Set to `false` for cost-\n * controlled deployments where billable warehouse starts must not be\n * triggered by user requests; in that case `STOPPED` surfaces as a\n * `ConfigurationError`.\n */\n autoStartWarehouse?: boolean;\n /**\n * Fail-fast ceiling (ms) for an `ARROW_STREAM` query to produce its first\n * byte (warehouse readiness + execute + first chunk). Past this, a stuck or\n * overloaded warehouse returns a `503` (`WAREHOUSE_UNAVAILABLE`) instead of\n * hanging until the client disconnects. Defaults to 2 min. Once the first\n * byte arrives the stream is not time-bounded.\n */\n arrowFirstByteTimeoutMs?: number;\n}\n\n/**\n * SQL warehouse lifecycle states surfaced by the analytics route.\n * Mirrors the states emitted by the Databricks SQL SDK (`sql.State`).\n */\nexport type WarehouseState =\n | \"RUNNING\"\n | \"STARTING\"\n | \"STOPPED\"\n | \"STOPPING\"\n | \"DELETED\"\n | \"DELETING\";\n\n/**\n * Snapshot of warehouse readiness streamed to the client over SSE before the\n * SQL result. Lets the UI render a \"warehouse starting…\" affordance instead\n * of a frozen spinner during cold starts.\n *\n * Note: the SDK's `health.summary` is intentionally NOT forwarded here. It's\n * free-form operator-oriented diagnostic text (cluster IDs, capacity-failure\n * reasons, internal RPC errors) that must not reach end users; it stays in\n * server-side telemetry only.\n */\nexport interface WarehouseStatus {\n state: WarehouseState;\n /** Milliseconds elapsed since the route began waiting for the warehouse. */\n elapsedMs: number;\n}\n\n/**\n * Discriminated union of every SSE message shape emitted by\n * `POST /api/analytics/query/:query_key`. Useful for typing the client-side\n * `onMessage` handler (and is the source of truth re-mirrored in\n * `appkit-ui` since that package can't depend on `appkit`).\n */\nexport type AnalyticsStreamMessage =\n | { type: \"warehouse_status\"; status: WarehouseStatus }\n | {\n type: \"result\";\n data?: unknown[];\n status?: unknown;\n statement_id?: string;\n metadata?: Record<string, MetricViewColumnDisplay>;\n }\n | {\n type: \"arrow\";\n statement_id: string;\n status: { state: string };\n }\n | { type: \"error\"; error: string; code?: string };\n\n/**\n * Supported response formats for analytics queries.\n *\n * \"JSON\" and \"ARROW\" are legacy aliases kept for backwards compatibility\n * with appkit/appkit-ui < 0.33.0 — safe to remove once no consumer is on\n * a pre-0.33.0 version. The route handler normalizes them to their\n * canonical equivalents before any downstream code reads the value.\n */\nexport type AnalyticsFormat =\n | \"JSON_ARRAY\"\n | \"ARROW_STREAM\"\n /** @deprecated Use \"JSON_ARRAY\". Safe to remove once no consumer is on appkit < 0.33.0. */\n | \"JSON\"\n /** @deprecated Use \"ARROW_STREAM\". Safe to remove once no consumer is on appkit < 0.33.0. */\n | \"ARROW\";\n\n/** Canonical (post-normalization) analytics format values. */\ntype CanonicalAnalyticsFormat = \"JSON_ARRAY\" | \"ARROW_STREAM\";\n\n/**\n * Map a (possibly legacy) AnalyticsFormat to its canonical form.\n * Legacy values come from appkit/appkit-ui < 0.33.0 and can be removed\n * along with the deprecated aliases once no such consumer remains.\n */\nexport function normalizeAnalyticsFormat(\n f: AnalyticsFormat,\n): CanonicalAnalyticsFormat {\n if (f === \"JSON\") return \"JSON_ARRAY\";\n if (f === \"ARROW\") return \"ARROW_STREAM\";\n return f;\n}\n\nexport interface IAnalyticsQueryRequest {\n parameters?: Record<string, any>;\n format?: AnalyticsFormat;\n}\n\nexport interface AnalyticsQueryResponse {\n chunk_index: number;\n row_offset: number;\n row_count: number;\n data: any[];\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Metric views — POST /api/analytics/metric/:key\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Execution lane for a registered metric view, derived from the entry's\n * `executor` in `definitions.json`:\n * - `\"sp\"` ← `executor: \"app_service_principal\"` — queried as the app\n * service principal (cache shared across all users).\n * - `\"obo\"` ← `executor: \"user\"` — queried on-behalf-of the requesting\n * user (per-user cache). OBO dispatch is wired in a later phase.\n */\nexport type MetricLane = \"sp\" | \"obo\";\n\n/**\n * A single registered metric view, loaded from `config/metric-views/definitions.json`.\n *\n * The registration carries only what the runtime needs to build and dispatch\n * SQL: the metric `key`, the three-part UC FQN `source`, and the `lane`. There\n * is intentionally NO build-time measure/dimension metadata here — the security\n * boundary is the grammar gate plus parameterized values, not a name allowlist,\n * so the runtime never enumerates known measures/dimensions.\n */\nexport interface MetricRegistration {\n key: string;\n source: string;\n lane: MetricLane;\n}\n\n/**\n * v1 filter operator vocabulary — exactly twelve names. The runtime tuple\n * `METRIC_FILTER_OPERATORS` (next to the validator in `metric.ts`) is the\n * server-side source of truth; this union mirrors it statically.\n */\nexport type MetricFilterOperatorName =\n | \"equals\"\n | \"notEquals\"\n | \"in\"\n | \"notIn\"\n | \"gt\"\n | \"gte\"\n | \"lt\"\n | \"lte\"\n | \"contains\"\n | \"notContains\"\n | \"set\"\n | \"notSet\";\n\n/**\n * A single filter predicate — the leaf node of the recursive\n * {@link MetricFilter} tree. `member` is a dimension name (grammar-gated, not\n * allowlisted); `values` is bound through parameterized `:f_<idx>` bind vars\n * and never interpolated into the SQL string.\n */\nexport interface MetricPredicate {\n member: string;\n operator: MetricFilterOperatorName;\n values?: ReadonlyArray<string | number>;\n}\n\n/**\n * Recursive filter expression for the metric-view request body: a leaf\n * {@link MetricPredicate} or an `{ and: [...] }` / `{ or: [...] }` group. The\n * shape is intentionally non-generic server-side — per-metric narrowing (if\n * any) lives client-side.\n */\nexport type MetricFilter =\n | MetricPredicate\n | { and: ReadonlyArray<MetricFilter> }\n | { or: ReadonlyArray<MetricFilter> };\n\n/**\n * Validated request body for `POST /api/analytics/metric/:key`.\n *\n * `measures` is required. `dimensions` drive `GROUP BY ALL`; `filter` is the\n * recursive structured predicate tree translated into a parameterized `WHERE`\n * clause. `timeGrain` buckets the single dimension named by `timeDimension`\n * via `date_trunc`; it requires `timeDimension`, and `timeDimension` must be\n * one of `dimensions` so it is selected and in `GROUP BY ALL`. Both tokens are\n * grammar-gated before they reach SQL.\n */\nexport interface IAnalyticsMetricRequest {\n measures: string[];\n dimensions?: string[];\n filter?: MetricFilter;\n timeGrain?: string;\n /**\n * The single dimension that `timeGrain` buckets via `date_trunc`. Must be\n * one of `dimensions` (so it is selected and in `GROUP BY ALL`) and is\n * required whenever `timeGrain` is set. Grammar-gated as a SQL identifier.\n */\n timeDimension?: string;\n limit?: number;\n format?: AnalyticsFormat;\n}\n"],"mappings":";;;;;;AA+GA,SAAgB,yBACd,GAC0B;AAC1B,KAAI,MAAM,OAAQ,QAAO;AACzB,KAAI,MAAM,QAAS,QAAO;AAC1B,QAAO"}
@@ -4,6 +4,7 @@ import { BasePlugin, BasePluginConfig, HttpMethod, IAppRequest, IAppResponse, IA
4
4
  import { CacheConfig, CacheEntry, CacheStorage } from "./cache.js";
5
5
  import { PluginExecuteConfig, PluginExecutionSettings, RetryConfig, StreamConfig, StreamExecuteHandler, StreamExecutionSettings, TelemetryConfig } from "./execute.js";
6
6
  import { GenieAttachmentResponse, GenieMessageResponse, GenieStatementResponse, GenieStreamEvent } from "./genie.js";
7
+ import { MetricViewColumnDisplay, MetricViewsMetadata } from "./metric-metadata.js";
7
8
  import { SQLBinaryMarker, SQLBooleanMarker, SQLDateMarker, SQLNumberMarker, SQLStringMarker, SQLTimestampMarker, SQLTypeMarker } from "./sql/types.js";
8
9
  import { isSQLTypeMarker, sql } from "./sql/helpers.js";
9
10
  import "./sse/analytics.js";
@@ -0,0 +1,24 @@
1
+ //#region ../shared/src/metric-metadata.d.ts
2
+ /**
3
+ * Per-column display metadata for a UC Metric View column, sourced from the
4
+ * YAML 1.1 `display_name`/`format` attributes plus the SQL type. Loose enough
5
+ * that an `as const` generated literal assigns to it.
6
+ */
7
+ interface MetricViewColumnDisplay {
8
+ type: string;
9
+ display_name?: string;
10
+ format?: string;
11
+ description?: string;
12
+ }
13
+ /**
14
+ * Build-time-generated metadata for every registered metric view, keyed by
15
+ * metric key. Injected into the analytics plugin via
16
+ * `analytics({ metricViewsMetadata })`.
17
+ */
18
+ type MetricViewsMetadata = Record<string, {
19
+ measures: Record<string, MetricViewColumnDisplay>;
20
+ dimensions: Record<string, MetricViewColumnDisplay>;
21
+ }>;
22
+ //#endregion
23
+ export { MetricViewColumnDisplay, MetricViewsMetadata };
24
+ //# sourceMappingURL=metric-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metric-metadata.d.ts","names":[],"sources":["../../../../shared/src/metric-metadata.ts"],"mappings":";;AAKA;;;;UAAiB,uBAAA;EACf,IAAA;EACA,YAAA;EACA,MAAA;EACA,WAAA;AAAA;AAQF;;;;;AAAA,KAAY,mBAAA,GAAsB,MAAA;EAG9B,QAAA,EAAU,MAAA,SAAe,uBAAA;EACzB,UAAA,EAAY,MAAA,SAAe,uBAAA;AAAA"}
@@ -25,7 +25,8 @@ const AnalyticsResultMessage = z.object({
25
25
  type: z.literal("result"),
26
26
  data: z.array(z.unknown()).optional(),
27
27
  status: z.unknown().optional(),
28
- statement_id: z.string().optional()
28
+ statement_id: z.string().optional(),
29
+ metadata: z.record(z.string(), z.unknown()).optional()
29
30
  });
30
31
  function makeResultMessage(data, extras = {}) {
31
32
  return {