Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f793e05
chore(appkit): metric-view route skeleton + measures-only SQL (PR2 ph…
atilafassina Jul 9, 2026
fd97356
chore(appkit): metric dimensions + structured filter engine (PR2 phas…
atilafassina Jul 9, 2026
9d2065c
chore(appkit): apply timeGrain via explicit timeDimension (PR2 phase 2b)
atilafassina Jul 9, 2026
a7ecda5
chore(appkit): metric OBO dispatch + cache-key isolation (PR2 phase 3)
atilafassina Jul 9, 2026
15d9371
chore(appkit): harden metric route lookup, arg uniqueness, sort key, …
atilafassina Jul 10, 2026
3ab36eb
chore(appkit): unify metric-view FQN grammar + quote at SQL interpola…
atilafassina Jul 10, 2026
00ec7e7
chore(appkit): load metric registry lazily with an mtime-validated cache
atilafassina Jul 10, 2026
deff6b8
chore(appkit): quote measure/dimension identifiers + close review-rou…
atilafassina Jul 10, 2026
9a9d92f
refactor(appkit): split metric runtime modules
atilafassina Jul 10, 2026
9a3bc06
refactor(appkit): derive metric operator sets from shared bases
atilafassina Jul 20, 2026
509c904
chore: adjust comments
atilafassina Jul 20, 2026
3932e53
docs: metric-view runtime endpoint (#484)
atilafassina Jul 21, 2026
af32812
fix(analytics): reject empty and-group in metric filter validation
atilafassina Jul 21, 2026
5f65039
refactor(analytics): route metric-view config reads through AppManager
atilafassina Jul 21, 2026
dce640c
fix(analytics): json-encode measures/dimensions in metric cache key
atilafassina Jul 21, 2026
57054ce
fix(analytics): render empty AND filter group as vacuous-true
atilafassina Jul 21, 2026
afe41e5
chore: adjust comments
atilafassina Jul 21, 2026
ea5d8a8
refactor(analytics): read metric-views.json per request, drop mtime c…
atilafassina Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/dev-playground/shared/appkit-types/metric-views.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,31 @@ declare module "@databricks/appkit-ui/react" {
measures: {
"active_accounts": {
type: "bigint";
display_name: "Active Accounts";
format: "#,##0";
};
"churn_rate": {
type: "decimal";
display_name: "Churn Rate";
};
"avg_ltv": {
type: "double";
display_name: "Average LTV";
format: "$#,##0.00";
};
};
dimensions: {
"segment": {
type: "string";
display_name: "Customer Segment";
};
"region": {
type: "string";
display_name: "Region";
};
"csm_email": {
type: "string";
display_name: "CSM Email";
};
};
};
Expand Down Expand Up @@ -80,27 +88,38 @@ declare module "@databricks/appkit-ui/react" {
measures: {
"mrr": {
type: "double";
display_name: "Monthly Recurring Revenue";
format: "$#,##0.00";
};
"arr": {
type: "double";
display_name: "Annual Recurring Revenue";
format: "$#,##0.00";
description: "Annualized contract value across all active subscriptions";
};
"new_arr": {
type: "double";
display_name: "New ARR";
format: "$#,##0.00";
};
"churned_arr": {
type: "double";
display_name: "Churned ARR";
format: "$#,##0.00";
};
};
dimensions: {
"region": {
type: "string";
display_name: "Region";
};
"segment": {
type: "string";
display_name: "Customer Segment";
};
"created_at": {
type: "timestamp_ltz";
display_name: "Subscription Start";
time_grain: readonly ["day", "hour", "minute", "month", "quarter", "week", "year"];
};
};
Expand Down
211 changes: 178 additions & 33 deletions docs/docs/plugins/analytics.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/static/schemas/metric-source.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 66 additions & 3 deletions packages/appkit/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,26 @@ interface FileSystemAdapter {
}

export class AppManager {
private readonly queriesDir = path.resolve(process.cwd(), "config/queries");
private readonly _queriesDir: string;

constructor(
queriesDir: string = path.resolve(process.cwd(), "config/queries"),
) {
this._queriesDir = queriesDir;
}

get queriesDir(): string {
return this._queriesDir;
}

/**
* Whether `req` is a dev-tunnel (`?dev`) request. Internal `?dev` predicate
* shared by {@link createFsAdapter} (tunnel-vs-`fs` branch) and
* {@link isNotFoundError} (dev's message-match not-found classification).
*/
private isDevRequest(req?: RequestLike): boolean {
return req?.query?.dev !== undefined;
}

/**
* Validates that a file path is within the queries directory
Expand All @@ -53,7 +72,7 @@ export class AppManager {
req?: RequestLike,
devFileReader?: DevFileReader,
): FileSystemAdapter {
const isDevMode = req?.query?.dev !== undefined;
const isDevMode = this.isDevRequest(req);

if (isDevMode && devFileReader && req) {
// Dev mode: use WebSocket tunnel to read from local filesystem
Expand Down Expand Up @@ -155,6 +174,50 @@ export class AppManager {
return null;
}
}

/**
* Read a single config file from the queries directory, dev-tunnel-aware.
*
* @param fileName - File name (or relative path) within the queries directory.
* @param req - Optional request object to detect dev mode.
* @param devFileReader - Optional DevFileReader to read via the WebSocket tunnel.
* @returns The raw file contents, or `null` when the file is absent / the path is rejected.
*/
async readConfigFile(
fileName: string,
req?: RequestLike,
devFileReader?: DevFileReader,
): Promise<string | null> {
// Traversal guard: refuse to read outside the queries directory.
const resolvedPath = this.validatePath(fileName);
if (!resolvedPath) {
return null;
}

const fsAdapter = this.createFsAdapter(req, devFileReader);

try {
return await fsAdapter.readFile(resolvedPath);
} catch (error) {
if (this.isNotFoundError(error, req)) {
return null;
}
// Any other error (permission / IO / tunnel) is fatal: propagate so the
// caller can distinguish it from a dormant (absent) config.
throw error;
}
}

private isNotFoundError(error: unknown, req?: RequestLike): boolean {
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
return true;
}
if (this.isDevRequest(req)) {
const message = (error as Error)?.message ?? "";
return /ENOENT|no such file/i.test(message);
}
return false;
}
}

export type { DevFileReader };
export type { DevFileReader, RequestLike };
157 changes: 157 additions & 0 deletions packages/appkit/src/app/tests/read-config-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import type { DevFileReader } from "../index";
import { AppManager } from "../index";

// NOTE: unlike the sibling `app.test.ts`, this spec deliberately does NOT mock
// `node:fs/promises` — `readConfigFile` is exercised against real temp files so
// the not-found-vs-throw distinction is driven by genuine errno codes.

describe("AppManager.readConfigFile", () => {
let tmpDir: string;
let appManager: AppManager;

beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "appmgr-config-"));
appManager = new AppManager(tmpDir);
});

afterEach(async () => {
vi.restoreAllMocks();
await fs.rm(tmpDir, { recursive: true, force: true });
});

describe("dir override + getter", () => {
test("queriesDir getter exposes the overridden directory", () => {
expect(appManager.queriesDir).toBe(tmpDir);
});

test("defaults to <cwd>/config/queries when no override is given", () => {
const defaultManager = new AppManager();
expect(defaultManager.queriesDir).toBe(
path.resolve(process.cwd(), "config/queries"),
);
});
});

describe("production mode (direct fs)", () => {
test("returns file contents for an existing file", async () => {
await fs.writeFile(
path.join(tmpDir, "metric-views.json"),
'{"hello":"world"}',
"utf8",
);

const result = await appManager.readConfigFile("metric-views.json");
expect(result).toBe('{"hello":"world"}');
});

test("returns null for a genuine not-found (ENOENT)", async () => {
const result = await appManager.readConfigFile("does-not-exist.json");
expect(result).toBeNull();
});

test("throws (does NOT return null) for a non-ENOENT error", async () => {
// Reading a directory as a file rejects with EISDIR — a real, non-ENOENT
// error that must propagate rather than be swallowed to null.
await fs.mkdir(path.join(tmpDir, "a-directory"));

await expect(appManager.readConfigFile("a-directory")).rejects.toThrow();
});

test("throws for a simulated EACCES (permission) error", async () => {
const err = Object.assign(new Error("permission denied"), {
code: "EACCES",
});
vi.spyOn(fs, "readFile").mockRejectedValueOnce(err);

await expect(
appManager.readConfigFile("metric-views.json"),
).rejects.toThrow("permission denied");
});

test("reads via direct fs (not the dev reader) without ?dev", async () => {
await fs.writeFile(
path.join(tmpDir, "metric-views.json"),
"prod-contents",
"utf8",
);
const devFileReader: DevFileReader = {
readdir: vi.fn(),
readFile: vi.fn(),
};

const result = await appManager.readConfigFile(
"metric-views.json",
{ query: {}, headers: {} },
devFileReader,
);

expect(result).toBe("prod-contents");
expect(devFileReader.readFile).not.toHaveBeenCalled();
});
});

describe("path traversal protection", () => {
test("returns null and does not read outside the queries dir", async () => {
const readSpy = vi.spyOn(fs, "readFile");
const result = await appManager.readConfigFile("../../etc/passwd");

expect(result).toBeNull();
expect(readSpy).not.toHaveBeenCalled();
});
});

describe("dev mode (WebSocket tunnel)", () => {
const devReq = { query: { dev: "true" }, headers: {} };

test("reads via devFileReader in dev mode", async () => {
const devFileReader: DevFileReader = {
readdir: vi.fn(),
readFile: vi.fn().mockResolvedValue("dev-contents"),
};

const result = await appManager.readConfigFile(
"metric-views.json",
devReq,
devFileReader,
);

expect(result).toBe("dev-contents");
expect(devFileReader.readFile).toHaveBeenCalledWith(
expect.stringContaining("metric-views.json"),
devReq,
);
});

test("returns null for the dev tunnel's not-found signal (best-effort)", async () => {
const devFileReader: DevFileReader = {
readdir: vi.fn(),
readFile: vi
.fn()
.mockRejectedValue(new Error("ENOENT: no such file or directory")),
};

const result = await appManager.readConfigFile(
"metric-views.json",
devReq,
devFileReader,
);

expect(result).toBeNull();
});

test("throws for a non-not-found dev tunnel error", async () => {
const devFileReader: DevFileReader = {
readdir: vi.fn(),
readFile: vi.fn().mockRejectedValue(new Error("tunnel disconnected")),
};

await expect(
appManager.readConfigFile("metric-views.json", devReq, devFileReader),
).rejects.toThrow("tunnel disconnected");
});
});
});
Loading
Loading