Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,12 @@ The SDK has built-in SSE support with automatic reconnection:
### Analytics Query Pattern

The AnalyticsPlugin provides SQL query execution:
- Queries stored in `config/queries/<query_key>.sql`
- Queries stored in `config/queries/`
- Query file naming determines execution context:
- `<query_key>.sql` - Executes as service principal (shared cache)
- `<query_key>.obo.sql` - Executes as user (OBO = On-Behalf-Of, per-user cache)
- All queries should be parameterized (use placeholders)
- POST `/api/analytics/:query_key` - Execute query with parameters
- POST `/api/analytics/query/:query_key` - Execute query with parameters
- Built-in caching with configurable TTL
- Databricks SQL Warehouse connector for execution

Expand Down
2 changes: 1 addition & 1 deletion apps/dev-playground/client/src/routes/analytics.route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function AnalyticsRoute() {
data: untaggedAppsData,
loading: untaggedAppsLoading,
error: untaggedAppsError,
} = useAnalyticsQuery("untagged_apps", untaggedAppsParams, { asUser: true });
} = useAnalyticsQuery("untagged_apps", untaggedAppsParams);

const metrics = useMemo(() => {
if (!summaryDataRaw || summaryDataRaw.length === 0) {
Expand Down
7 changes: 5 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,12 @@ WHERE workspace_id = :workspaceId
HTTP endpoints exposed (mounted under `/api/analytics`):

- `POST /api/analytics/query/:query_key`
- `POST /api/analytics/users/me/query/:query_key`
- `GET /api/analytics/arrow-result/:jobId`
- `GET /api/analytics/users/me/arrow-result/:jobId`

**Query file naming convention determines execution context:**

- `config/queries/<query_key>.sql` - Executes as service principal (shared cache)
- `config/queries/<query_key>.obo.sql` - Executes as user (OBO = On-Behalf-Of, per-user cache)

Formats:

Expand Down
3 changes: 0 additions & 3 deletions packages/appkit-ui/src/react/charts/create-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export function createChart<TProps extends UnifiedChartProps>(
parameters,
format,
transformer,
asUser,
// Data props
data,
// Common props
Expand All @@ -42,7 +41,6 @@ export function createChart<TProps extends UnifiedChartProps>(
parameters?: Record<string, unknown>;
format?: string;
transformer?: unknown;
asUser?: boolean;
data?: unknown;
height?: number;
className?: string;
Expand All @@ -58,7 +56,6 @@ export function createChart<TProps extends UnifiedChartProps>(
parameters,
format,
transformer,
asUser,
height,
className,
ariaLabel,
Expand Down
6 changes: 0 additions & 6 deletions packages/appkit-ui/src/react/charts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,6 @@ export interface QueryProps extends ChartBaseProps {
format?: DataFormat;
/** Transform raw data before rendering */
transformer?: <T>(data: T) => T;
/**
* Whether to execute the query as the current user
* @default false
*/
asUser?: boolean;

// Discriminator: cannot use direct data with query
data?: never;
}
Expand Down
5 changes: 0 additions & 5 deletions packages/appkit-ui/src/react/charts/wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { isArrowTable } from "./types";
// ============================================================================

interface ChartWrapperQueryProps {
/** Whether to execute the query as a user. Default is false. */
asUser?: boolean;
/** Analytics query key */
queryKey: string;
/** Query parameters */
Expand Down Expand Up @@ -61,7 +59,6 @@ function QueryModeContent({
parameters,
format,
transformer,
asUser,
height,
className,
ariaLabel,
Expand All @@ -73,7 +70,6 @@ function QueryModeContent({
parameters,
format,
transformer,
asUser,
});

if (loading) return <LoadingSkeleton height={height ?? 300} />;
Expand Down Expand Up @@ -184,7 +180,6 @@ export function ChartWrapper(props: ChartWrapperProps) {
parameters={props.parameters}
format={props.format}
transformer={props.transformer}
asUser={props.asUser}
height={height}
className={className}
ariaLabel={ariaLabel}
Expand Down
3 changes: 0 additions & 3 deletions packages/appkit-ui/src/react/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ export interface UseAnalyticsQueryOptions<F extends AnalyticsFormat = "JSON"> {

/** Whether to automatically start the query when the hook is mounted. Default is true. */
autoStart?: boolean;

/** Whether to execute the query as a user. Default is false. */
asUser?: boolean;
}

/** Result state returned by useAnalyticsQuery */
Expand Down
11 changes: 6 additions & 5 deletions packages/appkit-ui/src/react/hooks/use-analytics-query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArrowClient, connectSSE } from "@/js";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ArrowClient, connectSSE } from "@/js";
import type {
AnalyticsFormat,
InferParams,
Expand Down Expand Up @@ -30,6 +30,10 @@ function getArrowStreamUrl(id: string) {
* - `format: "JSON"` (default): Returns typed array from QueryRegistry
* - `format: "ARROW"`: Returns TypedArrowTable with row type preserved
*
* Note: User context execution is determined by query file naming:
* - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)
* - `queryKey.sql`: Executes as service principal
*
* @param queryKey - Analytics query identifier
* @param parameters - Query parameters (type-safe based on QueryRegistry)
* @param options - Analytics query settings including format
Expand Down Expand Up @@ -59,12 +63,9 @@ export function useAnalyticsQuery<
const format = options?.format ?? "JSON";
const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;
const autoStart = options?.autoStart ?? true;
const asUser = options?.asUser ?? false;

const devMode = getDevMode();
const urlSuffix = asUser
? `/api/analytics/users/me/query/${encodeURIComponent(queryKey)}${devMode}`
: `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;
const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;

type ResultType = InferResultByFormat<T, K, F>;
const [data, setData] = useState<ResultType | null>(null);
Expand Down
11 changes: 1 addition & 10 deletions packages/appkit-ui/src/react/hooks/use-chart-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export interface UseChartDataOptions {
format?: DataFormat;
/** Transform data after fetching */
transformer?: <T>(data: T) => T;
/** Whether to execute the query as the current user. @default false */
asUser?: boolean;
}

export interface UseChartDataResult {
Expand Down Expand Up @@ -104,13 +102,7 @@ function resolveFormat(
* ```
*/
export function useChartData(options: UseChartDataOptions): UseChartDataResult {
const {
queryKey,
parameters,
format = "auto",
transformer,
asUser = false,
} = options;
const { queryKey, parameters, format = "auto", transformer } = options;

// Resolve the format to use
const resolvedFormat = useMemo(
Expand All @@ -128,7 +120,6 @@ export function useChartData(options: UseChartDataOptions): UseChartDataResult {
} = useAnalyticsQuery(queryKey, parameters, {
autoStart: true,
format: resolvedFormat,
asUser,
});

// Process and transform data
Expand Down
3 changes: 0 additions & 3 deletions packages/appkit-ui/src/react/table/table-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const CHECKBOX_COLUMN_WIDTH = 40;
* @param props.queryKey - The query key to fetch the data
* @param props.parameters - The parameters to pass to the query
* @param props.transformer - Optional function to transform raw data before creating table
* @param props.asUser - Whether to execute the query as a user. Default is false.
* @param props.children - Render function that receives the TanStack Table instance
* @param props.className - Optional CSS class name for the wrapper
* @param props.ariaLabel - Optional accessibility label
Expand All @@ -60,7 +59,6 @@ export function TableWrapper<TRaw = any, TProcessed = any>(
queryKey,
parameters,
transformer,
asUser = false,
children,
className,
ariaLabel,
Expand All @@ -78,7 +76,6 @@ export function TableWrapper<TRaw = any, TProcessed = any>(
const { data, loading, error } = useAnalyticsQuery<TRaw[]>(
queryKey,
parameters,
{ asUser },
);

useEffect(() => {
Expand Down
2 changes: 0 additions & 2 deletions packages/appkit-ui/src/react/table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export interface TableWrapperProps<TRaw = any, TProcessed = any> {
parameters: Record<string, any>;
/** Optional function to transform raw data before creating table */
transformer?: (data: TRaw[]) => TProcessed[];
/** Whether to execute the query as a user. Default is false. */
asUser?: boolean;
/** Render function that receives the TanStack Table instance */
children: (data: Table<TProcessed>) => React.ReactNode;
/** Optional CSS class name for the wrapper */
Expand Down
67 changes: 26 additions & 41 deletions packages/appkit/src/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,6 @@ export class AnalyticsPlugin extends Plugin {
await this._handleQueryRoute(req, res);
},
});

// User context endpoints - use asUser(req) to execute with user's identity
this.route(router, {
name: "arrowAsUser",
method: "get",
path: "/users/me/arrow-result/:jobId",
handler: async (req: express.Request, res: express.Response) => {
await this.asUser(req)._handleArrowRoute(req, res);
},
});

this.route<AnalyticsQueryResponse>(router, {
name: "queryAsUser",
method: "post",
path: "/users/me/query/:query_key",
handler: async (req: express.Request, res: express.Response) => {
await this.asUser(req)._handleQueryRoute(req, res);
},
});
}

/**
Expand Down Expand Up @@ -149,38 +130,42 @@ export class AnalyticsPlugin extends Plugin {
plugin: this.name,
});

const queryParameters =
format === "ARROW"
? {
formatParameters: {
disposition: "EXTERNAL_LINKS",
format: "ARROW_STREAM",
},
type: "arrow",
}
: {
type: "result",
};

// Get user key from current context (automatically includes user ID when in user context)
const userKey = getCurrentUserId();

if (!query_key) {
res.status(400).json({ error: "query_key is required" });
return;
}

const query = await this.app.getAppQuery(
const queryResult = await this.app.getAppQuery(
query_key,
req,
this.devFileReader,
);

if (!query) {
if (!queryResult) {
res.status(404).json({ error: "Query not found" });
return;
}

const { query, isAsUser } = queryResult;

// get execution context - user-scoped if .obo.sql, otherwise service principal
const executor = isAsUser ? this.asUser(req) : this;
const userKey = getCurrentUserId();
const executorKey = isAsUser ? userKey : "global";

const queryParameters =
format === "ARROW"
? {
formatParameters: {
disposition: "EXTERNAL_LINKS",
format: "ARROW_STREAM",
},
type: "arrow",
}
: {
type: "result",
};

const hashedQuery = this.queryProcessor.hashQuery(query);

const defaultConfig: PluginExecuteConfig = {
Expand All @@ -193,7 +178,7 @@ export class AnalyticsPlugin extends Plugin {
JSON.stringify(parameters),
JSON.stringify(format),
hashedQuery,
userKey,
executorKey,
],
},
};
Expand All @@ -202,15 +187,15 @@ export class AnalyticsPlugin extends Plugin {
default: defaultConfig,
};

await this.executeStream(
await executor.executeStream(
res,
async (signal) => {
const processedParams = await this.queryProcessor.processQueryParams(
query,
parameters,
);

const result = await this.query(
const result = await executor.query(
query,
processedParams,
queryParameters.formatParameters,
Expand All @@ -220,7 +205,7 @@ export class AnalyticsPlugin extends Plugin {
return { type: queryParameters.type, ...result };
},
streamExecutionSettings,
userKey,
executorKey,
);
}

Expand Down
Loading