data layer
This commit is contained in:
122
data-layer/console/src/api/client.ts
Normal file
122
data-layer/console/src/api/client.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// Thin fetch wrapper for the analytics API. Throws ApiError on non-2xx.
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
field?: string;
|
||||
constructor(status: number, message: string, field?: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
const BASE = import.meta.env.VITE_ANALYTICS_BASE_URL ?? '/api/analytics';
|
||||
|
||||
async function request<T>(path: string, workspaceID: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'X-Workspace-Id': workspaceID,
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
const text = await res.text();
|
||||
const data = text ? safeJSON(text) : undefined;
|
||||
if (!res.ok) {
|
||||
const msg = (data as { error?: string })?.error ?? res.statusText;
|
||||
const field = (data as { field?: string })?.field;
|
||||
throw new ApiError(res.status, msg, field);
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
function safeJSON(text: string): unknown {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Common shapes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface QueryResult {
|
||||
columns: string[];
|
||||
rows: unknown[][];
|
||||
row_count: number;
|
||||
duration_ms: number;
|
||||
cache_hit: boolean;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
user_id?: string;
|
||||
anonymous_ids?: string[];
|
||||
traits?: Record<string, unknown>;
|
||||
first_seen_at: string;
|
||||
last_seen_at: string;
|
||||
}
|
||||
|
||||
export interface SavedQuery {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
owner_id?: string;
|
||||
name: string;
|
||||
kind: 'events' | 'sql' | 'funnel' | 'retention' | 'session';
|
||||
spec: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Endpoints (current workspace passed by each caller)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const analytics = (workspaceID: string) => ({
|
||||
health: () => request<{ status: string }>('/health', workspaceID),
|
||||
ready: () => request<{ status: string }>('/ready', workspaceID),
|
||||
|
||||
queryEvents: (body: {
|
||||
table: 'events_track' | 'events_identify' | 'events_page' | 'events_group';
|
||||
from: string; to: string;
|
||||
user_id?: string; anonymous_id?: string; event?: string;
|
||||
limit?: number; offset?: number;
|
||||
}) => request<QueryResult>('/query/events', workspaceID, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
querySQL: (body: { sql: string }) =>
|
||||
request<QueryResult>('/query/sql', workspaceID, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
queryFunnel: (body: { steps: string[]; from: string; to: string; window_seconds: number }) =>
|
||||
request<QueryResult>('/query/funnel', workspaceID, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
queryRetention: (body: {
|
||||
initial_event: string; return_event: string;
|
||||
from: string; to: string; periods?: number;
|
||||
}) => request<QueryResult>('/query/retention', workspaceID, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
querySession: (body: {
|
||||
from: string; to: string;
|
||||
timeout_seconds?: number; user_id?: string;
|
||||
limit?: number; offset?: number;
|
||||
}) => request<QueryResult>('/query/session', workspaceID, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
getProfile: (id: string) =>
|
||||
request<Profile>(`/profiles/${id}`, workspaceID),
|
||||
|
||||
getProfileTimeline: (id: string, limit = 100, offset = 0) =>
|
||||
request<QueryResult>(`/profiles/${id}/events?limit=${limit}&offset=${offset}`, workspaceID),
|
||||
|
||||
listSavedQueries: () =>
|
||||
request<{ items: SavedQuery[]; limit: number; offset: number }>('/queries', workspaceID),
|
||||
|
||||
createSavedQuery: (body: { name: string; kind: SavedQuery['kind']; spec: Record<string, unknown> }) =>
|
||||
request<SavedQuery>('/queries', workspaceID, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
deleteSavedQuery: (id: string) =>
|
||||
request<void>(`/queries/${id}`, workspaceID, { method: 'DELETE' }),
|
||||
});
|
||||
Reference in New Issue
Block a user