@oxy-hq/sdk 2.0.1 → 2.3.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.
- package/README.md +9 -1
- package/dist/email.cjs +14 -0
- package/dist/email.cjs.map +1 -0
- package/dist/email.d.cts +8 -0
- package/dist/email.d.cts.map +1 -0
- package/dist/email.d.mts +8 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +13 -0
- package/dist/email.mjs.map +1 -0
- package/dist/index.cjs +877 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +731 -25
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +731 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +872 -186
- package/dist/index.mjs.map +1 -1
- package/package.json +33 -11
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
// @oxy/sdk - TypeScript SDK for Oxy data platform
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
|
|
5
|
+
//#region src/anomalies.ts
|
|
6
|
+
/**
|
|
7
|
+
* Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`
|
|
8
|
+
* rather than instantiating directly — the getter wires the request helper
|
|
9
|
+
* so auth, timeout, and branch propagation come along for free.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const { anomalies } = await client.anomalies.list({ status: "new" });
|
|
14
|
+
* for (const a of anomalies) {
|
|
15
|
+
* console.log(a.label ?? a.measure, a.severity, a.z_score.toFixed(2));
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
var AnomaliesClient = class {
|
|
20
|
+
constructor(config, request) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.request = request;
|
|
23
|
+
}
|
|
24
|
+
path(suffix) {
|
|
25
|
+
return `/${this.config.projectId}/semantic/anomalies${suffix}`;
|
|
26
|
+
}
|
|
27
|
+
buildQuery(extra = {}) {
|
|
28
|
+
const params = { ...extra };
|
|
29
|
+
if (this.config.branch) params.branch = this.config.branch;
|
|
30
|
+
const qs = new URLSearchParams(params).toString();
|
|
31
|
+
return qs ? `?${qs}` : "";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* List anomalies in the inbox, newest first.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // Open / unresolved anomalies only
|
|
39
|
+
* const { anomalies } = await client.anomalies.list({ status: "new" });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
async list(options = {}) {
|
|
43
|
+
const extra = {};
|
|
44
|
+
if (options.status) extra.status = options.status;
|
|
45
|
+
if (options.limit) extra.limit = String(options.limit);
|
|
46
|
+
return this.request(this.path(this.buildQuery(extra)));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Trigger a full scan. Iterates every `.monitor.yml` entry in the
|
|
50
|
+
* workspace, runs the detector, and upserts matching rows into the
|
|
51
|
+
* inbox. Returns counts of scanned / failed / persisted.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* // Scan against a known-good reference date (matches the seed dataset)
|
|
56
|
+
* const result = await client.anomalies.scan({ as_of: "2025-12-15" });
|
|
57
|
+
* console.log(`${result.anomalies_persisted} anomalies detected`);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
async scan(options = {}) {
|
|
61
|
+
const extra = {};
|
|
62
|
+
if (options.as_of) extra.as_of = options.as_of;
|
|
63
|
+
return this.request(this.path(`/scan${this.buildQuery(extra)}`), { method: "POST" });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Update an anomaly's status (acknowledge / dismiss / re-open).
|
|
67
|
+
*/
|
|
68
|
+
async updateStatus(anomalyId, status) {
|
|
69
|
+
const query = this.buildQuery();
|
|
70
|
+
return this.request(this.path(`/${encodeURIComponent(anomalyId)}/status${query}`), {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify({ status })
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Run the metric-tree `explain` for an anomaly and cache the result on
|
|
77
|
+
* the row. Subsequent calls return the cached `ExplainResult` instantly.
|
|
78
|
+
*/
|
|
79
|
+
async explain(anomalyId) {
|
|
80
|
+
const query = this.buildQuery();
|
|
81
|
+
return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`), { method: "POST" });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
4
86
|
//#region src/customer-app/logger.ts
|
|
5
87
|
let activeLogger = createConsoleLogger();
|
|
6
88
|
/** Replace the global logger. Pass `null` to silence everything. */
|
|
@@ -65,6 +147,52 @@ async function getCustomerAppDebug(resolved) {
|
|
|
65
147
|
|
|
66
148
|
//#endregion
|
|
67
149
|
//#region src/customer-app/errors.ts
|
|
150
|
+
/**
|
|
151
|
+
* Error thrown by all customer-app hooks when an API call returns a
|
|
152
|
+
* non-2xx response. Carries the structured `code` + `hint` the server
|
|
153
|
+
* emits so bundle UIs can render an actionable message instead of
|
|
154
|
+
* "404: { ...json... }".
|
|
155
|
+
*/
|
|
156
|
+
var OxyApiError = class extends Error {
|
|
157
|
+
constructor(opts) {
|
|
158
|
+
const base = opts.message || `HTTP ${opts.status}`;
|
|
159
|
+
const code = opts.code ? ` [${opts.code}]` : "";
|
|
160
|
+
const hint = opts.hint ? `\n\n${opts.hint}` : "";
|
|
161
|
+
super(`${base}${code}${hint}`);
|
|
162
|
+
this.name = "OxyApiError";
|
|
163
|
+
this.status = opts.status;
|
|
164
|
+
this.code = opts.code ?? null;
|
|
165
|
+
this.hint = opts.hint ?? null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Read a non-2xx response from oxy and return an `OxyApiError`.
|
|
170
|
+
* Parses the JSON envelope when present; falls back to raw text
|
|
171
|
+
* (truncated to 240 chars so a runaway HTML error page doesn't
|
|
172
|
+
* dominate the bundle UI).
|
|
173
|
+
*/
|
|
174
|
+
async function apiErrorFromResponse(resp) {
|
|
175
|
+
let body;
|
|
176
|
+
let raw = "";
|
|
177
|
+
try {
|
|
178
|
+
raw = await resp.text();
|
|
179
|
+
body = raw ? JSON.parse(raw) : void 0;
|
|
180
|
+
} catch {}
|
|
181
|
+
if (body && typeof body === "object") {
|
|
182
|
+
const b = body;
|
|
183
|
+
return new OxyApiError({
|
|
184
|
+
status: resp.status,
|
|
185
|
+
message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
|
|
186
|
+
code: typeof b.code === "string" ? b.code : null,
|
|
187
|
+
hint: typeof b.hint === "string" ? b.hint : null
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
|
|
191
|
+
return new OxyApiError({
|
|
192
|
+
status: resp.status,
|
|
193
|
+
message: snippet || `HTTP ${resp.status}`
|
|
194
|
+
});
|
|
195
|
+
}
|
|
68
196
|
const ARCH_DOC = "internal-docs/customer-apps.md";
|
|
69
197
|
/** Interpret a thrown error as a structured report for UI display. */
|
|
70
198
|
function interpretCustomerAppError(err) {
|
|
@@ -255,13 +383,174 @@ function validateManifest(raw, url) {
|
|
|
255
383
|
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
256
384
|
slug: raw.slug,
|
|
257
385
|
orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
|
|
258
|
-
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
|
|
386
|
+
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
|
|
387
|
+
functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
|
|
259
388
|
};
|
|
260
389
|
}
|
|
390
|
+
const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
391
|
+
/**
|
|
392
|
+
* Validate the optional `functions` map. Each key is a function name;
|
|
393
|
+
* each value declares how the function is invoked. Mirrors the
|
|
394
|
+
* server-side validation in `customer_apps_publish.rs` so a bad
|
|
395
|
+
* manifest fails at build, not at publish.
|
|
396
|
+
*/
|
|
397
|
+
function validateFunctions(raw) {
|
|
398
|
+
if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
|
|
399
|
+
const out = {};
|
|
400
|
+
for (const [fnName, value] of Object.entries(raw)) {
|
|
401
|
+
if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
|
|
402
|
+
if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
|
|
403
|
+
const fn = {};
|
|
404
|
+
if (value.entry !== void 0) {
|
|
405
|
+
if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
|
|
406
|
+
fn.entry = value.entry;
|
|
407
|
+
}
|
|
408
|
+
if (value.schedule !== void 0) {
|
|
409
|
+
if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
|
|
410
|
+
fn.schedule = value.schedule;
|
|
411
|
+
}
|
|
412
|
+
if (value.timezone !== void 0) {
|
|
413
|
+
if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
|
|
414
|
+
fn.timezone = value.timezone;
|
|
415
|
+
}
|
|
416
|
+
if (value.route !== void 0) {
|
|
417
|
+
if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
|
|
418
|
+
fn.route = value.route;
|
|
419
|
+
}
|
|
420
|
+
if (value.airwayStep !== void 0) {
|
|
421
|
+
const step = value.airwayStep;
|
|
422
|
+
if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
|
|
423
|
+
fn.airwayStep = {
|
|
424
|
+
pipeline: step.pipeline,
|
|
425
|
+
resource: step.resource
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (value.timeoutSeconds !== void 0) {
|
|
429
|
+
const t = value.timeoutSeconds;
|
|
430
|
+
if (typeof t !== "number" || !Number.isInteger(t) || t < 1 || t > 300) throw new Error(`oxy-app.json: function "${fnName}" \`timeoutSeconds\` must be an integer in [1, 300]`);
|
|
431
|
+
fn.timeoutSeconds = t;
|
|
432
|
+
}
|
|
433
|
+
if (value.cache !== void 0) {
|
|
434
|
+
const c = value.cache;
|
|
435
|
+
if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
|
|
436
|
+
if (c.ttlSeconds !== void 0) {
|
|
437
|
+
const ttl = c.ttlSeconds;
|
|
438
|
+
if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
|
|
439
|
+
fn.cache = { ttlSeconds: ttl };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (value.retries !== void 0) {
|
|
443
|
+
const r = value.retries;
|
|
444
|
+
if (!isRecord(r)) throw new Error(`oxy-app.json: function "${fnName}" \`retries\` must be an object`);
|
|
445
|
+
const retries = {};
|
|
446
|
+
for (const key of [
|
|
447
|
+
"maxAttempts",
|
|
448
|
+
"minTimeoutMs",
|
|
449
|
+
"maxTimeoutMs"
|
|
450
|
+
]) {
|
|
451
|
+
const n = r[key];
|
|
452
|
+
if (n !== void 0) {
|
|
453
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw new Error(`oxy-app.json: function "${fnName}" \`retries.${key}\` must be a positive integer`);
|
|
454
|
+
retries[key] = n;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
fn.retries = retries;
|
|
458
|
+
}
|
|
459
|
+
if (value.inputExample !== void 0) fn.inputExample = value.inputExample;
|
|
460
|
+
const hasSchedule = fn.schedule !== void 0;
|
|
461
|
+
const hasAirway = fn.airwayStep !== void 0;
|
|
462
|
+
if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
|
|
463
|
+
out[fnName] = fn;
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
261
467
|
function isRecord(v) {
|
|
262
468
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
263
469
|
}
|
|
264
470
|
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/customer-app/function-invoke.ts
|
|
473
|
+
const inflight$1 = /* @__PURE__ */ new Map();
|
|
474
|
+
/**
|
|
475
|
+
* Dedup key for an invocation: function name + its (stable-serialized) body,
|
|
476
|
+
* joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
|
|
477
|
+
* the separator can never collide with a name.
|
|
478
|
+
*/
|
|
479
|
+
function functionInvokeKey(name, body) {
|
|
480
|
+
return `${name}\n${JSON.stringify(body ?? {})}`;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Run `run()` unless an identical invocation is already in flight, in which
|
|
484
|
+
* case share its promise. The entry is removed once the promise settles — so
|
|
485
|
+
* this dedups concurrency only, it does NOT memoize the result.
|
|
486
|
+
*/
|
|
487
|
+
function sharedFunctionInvoke(key, run) {
|
|
488
|
+
const existing = inflight$1.get(key);
|
|
489
|
+
if (existing) return existing;
|
|
490
|
+
const p = run().finally(() => {
|
|
491
|
+
inflight$1.delete(key);
|
|
492
|
+
});
|
|
493
|
+
inflight$1.set(key, p);
|
|
494
|
+
return p;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/customer-app/function-sse.ts
|
|
499
|
+
/**
|
|
500
|
+
* Read a `text/event-stream` function response to completion. Resolves with the
|
|
501
|
+
* decoded result + captured logs, or rejects (with `.logs` attached) on an
|
|
502
|
+
* `event: error` frame / a stream that ends without a terminal event.
|
|
503
|
+
*/
|
|
504
|
+
async function readFunctionSseStream(resp) {
|
|
505
|
+
const reader = resp.body?.getReader();
|
|
506
|
+
if (!reader) throw new Error("function response has no body stream");
|
|
507
|
+
const decoder = new TextDecoder();
|
|
508
|
+
let buffer = "";
|
|
509
|
+
let dataPayload = "";
|
|
510
|
+
const logs = [];
|
|
511
|
+
const handleFrame = (frame) => {
|
|
512
|
+
let event = "message";
|
|
513
|
+
let data = "";
|
|
514
|
+
for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
515
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
516
|
+
if (event === "log") try {
|
|
517
|
+
const l = JSON.parse(data);
|
|
518
|
+
logs.push({
|
|
519
|
+
level: String(l.level ?? "info"),
|
|
520
|
+
message: String(l.message ?? "")
|
|
521
|
+
});
|
|
522
|
+
} catch {}
|
|
523
|
+
else if (event === "data") dataPayload = data;
|
|
524
|
+
else if (event === "done") return {
|
|
525
|
+
done: true,
|
|
526
|
+
value: dataPayload ? JSON.parse(dataPayload) : null
|
|
527
|
+
};
|
|
528
|
+
else if (event === "error") {
|
|
529
|
+
const payload = data ? JSON.parse(data) : {};
|
|
530
|
+
const err = new Error(payload.message || payload.error || "function invocation failed");
|
|
531
|
+
err.name = payload.error || "FunctionError";
|
|
532
|
+
err.logs = logs;
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
for (;;) {
|
|
537
|
+
const { done, value } = await reader.read();
|
|
538
|
+
if (done) break;
|
|
539
|
+
buffer += decoder.decode(value, { stream: true });
|
|
540
|
+
let sep;
|
|
541
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
542
|
+
const frame = buffer.slice(0, sep);
|
|
543
|
+
buffer = buffer.slice(sep + 2);
|
|
544
|
+
const result = handleFrame(frame);
|
|
545
|
+
if (result) return {
|
|
546
|
+
value: result.value,
|
|
547
|
+
logs
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
throw new Error("function stream ended without a terminal event");
|
|
552
|
+
}
|
|
553
|
+
|
|
265
554
|
//#endregion
|
|
266
555
|
//#region src/customer-app/interpolate.ts
|
|
267
556
|
/**
|
|
@@ -326,6 +615,50 @@ function isSafeLinkHref(raw) {
|
|
|
326
615
|
return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
|
|
327
616
|
}
|
|
328
617
|
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/customer-app/query-cache.ts
|
|
620
|
+
const SWR_TTL_MS = 3e4;
|
|
621
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
622
|
+
const cache = /* @__PURE__ */ new Map();
|
|
623
|
+
function queryKey(projectId, db, sql) {
|
|
624
|
+
return `${projectId} ${db ?? ""} ${sql}`;
|
|
625
|
+
}
|
|
626
|
+
function getCached(projectId, sql, db) {
|
|
627
|
+
const e = cache.get(queryKey(projectId, db, sql));
|
|
628
|
+
return e && Date.now() - e.at < SWR_TTL_MS ? e.data : void 0;
|
|
629
|
+
}
|
|
630
|
+
/** Fetch with in-flight dedup + cache. `force` bypasses the fresh-cache
|
|
631
|
+
* short-circuit (used by refetch) but still dedupes a concurrent in-flight. */
|
|
632
|
+
async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
|
|
633
|
+
const key = queryKey(projectId, db, sql);
|
|
634
|
+
if (!opts.force) {
|
|
635
|
+
const fresh = getCached(projectId, sql, db);
|
|
636
|
+
if (fresh) return fresh;
|
|
637
|
+
}
|
|
638
|
+
const existing = inflight.get(key);
|
|
639
|
+
if (existing) return existing;
|
|
640
|
+
const body = JSON.stringify({
|
|
641
|
+
sql,
|
|
642
|
+
...db ? { database: db } : {}
|
|
643
|
+
});
|
|
644
|
+
const p = (async () => {
|
|
645
|
+
const resp = await fetcher(`/api/projects/${projectId}/query`, {
|
|
646
|
+
method: "POST",
|
|
647
|
+
headers: { "content-type": "application/json" },
|
|
648
|
+
body
|
|
649
|
+
});
|
|
650
|
+
if (!resp.ok) throw await apiErrorFromResponse(resp);
|
|
651
|
+
const data = await resp.json();
|
|
652
|
+
cache.set(key, {
|
|
653
|
+
at: Date.now(),
|
|
654
|
+
data
|
|
655
|
+
});
|
|
656
|
+
return data;
|
|
657
|
+
})().finally(() => inflight.delete(key));
|
|
658
|
+
inflight.set(key, p);
|
|
659
|
+
return p;
|
|
660
|
+
}
|
|
661
|
+
|
|
329
662
|
//#endregion
|
|
330
663
|
//#region src/customer-app/react.tsx
|
|
331
664
|
function defaultFetcher(input, init) {
|
|
@@ -367,26 +700,55 @@ function OxyAppProvider(props) {
|
|
|
367
700
|
}, [manifestOptions, fetcher]);
|
|
368
701
|
if (state.status === "error" && state.error) {
|
|
369
702
|
const err = state.error;
|
|
370
|
-
return /* @__PURE__ */
|
|
703
|
+
return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
704
|
+
value: state,
|
|
705
|
+
children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
|
|
706
|
+
});
|
|
371
707
|
}
|
|
372
|
-
if (state.status === "loading") return /* @__PURE__ */
|
|
373
|
-
|
|
708
|
+
if (state.status === "loading") return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
709
|
+
value: state,
|
|
710
|
+
children: fallback ?? null
|
|
711
|
+
});
|
|
712
|
+
return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
713
|
+
value: state,
|
|
714
|
+
children
|
|
715
|
+
});
|
|
374
716
|
}
|
|
375
717
|
function defaultErrorFallback(err) {
|
|
376
|
-
return /* @__PURE__ */
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
718
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
719
|
+
style: {
|
|
720
|
+
margin: "2rem auto",
|
|
721
|
+
maxWidth: "640px",
|
|
722
|
+
padding: "1rem",
|
|
723
|
+
border: "1px solid #fca5a5",
|
|
724
|
+
background: "#fee2e2",
|
|
725
|
+
color: "#991b1b",
|
|
726
|
+
borderRadius: "8px",
|
|
727
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
728
|
+
fontSize: "14px"
|
|
729
|
+
},
|
|
730
|
+
children: [
|
|
731
|
+
/* @__PURE__ */ jsx("div", {
|
|
732
|
+
style: { fontWeight: 600 },
|
|
733
|
+
children: err.title
|
|
734
|
+
}),
|
|
735
|
+
/* @__PURE__ */ jsx("pre", {
|
|
736
|
+
style: {
|
|
737
|
+
fontSize: "12px",
|
|
738
|
+
marginTop: "4px"
|
|
739
|
+
},
|
|
740
|
+
children: err.message
|
|
741
|
+
}),
|
|
742
|
+
/* @__PURE__ */ jsxs("div", {
|
|
743
|
+
style: { marginTop: "12px" },
|
|
744
|
+
children: [
|
|
745
|
+
/* @__PURE__ */ jsx("strong", { children: "What to try:" }),
|
|
746
|
+
" ",
|
|
747
|
+
err.hint
|
|
748
|
+
]
|
|
749
|
+
})
|
|
750
|
+
]
|
|
751
|
+
});
|
|
390
752
|
}
|
|
391
753
|
/**
|
|
392
754
|
* Emit a one-time `console.warn` the first time a beta hook is
|
|
@@ -401,58 +763,6 @@ function warnBetaOnce(name) {
|
|
|
401
763
|
if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(`[@oxy-hq/sdk] \`${name}\` is in beta — interface and behavior may change. See https://github.com/oxy-hq/customer-apps for caveats and the migration guide.`);
|
|
402
764
|
}
|
|
403
765
|
/**
|
|
404
|
-
* Error thrown by all customer-app hooks when an API call returns a
|
|
405
|
-
* non-2xx response. Carries the structured `code` + `hint` the server
|
|
406
|
-
* emits so bundle UIs can render an actionable message instead of
|
|
407
|
-
* "404: { ...json... }".
|
|
408
|
-
*
|
|
409
|
-
* The server contract is documented in
|
|
410
|
-
* `crates/app/src/server/api/projects/agent_ask.rs` and
|
|
411
|
-
* `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
|
|
412
|
-
* Hooks that previously wrapped the raw text in `new Error()` now
|
|
413
|
-
* throw this type instead.
|
|
414
|
-
*/
|
|
415
|
-
var OxyApiError = class extends Error {
|
|
416
|
-
constructor(opts) {
|
|
417
|
-
const base = opts.message || `HTTP ${opts.status}`;
|
|
418
|
-
const code = opts.code ? ` [${opts.code}]` : "";
|
|
419
|
-
const hint = opts.hint ? `\n\n${opts.hint}` : "";
|
|
420
|
-
super(`${base}${code}${hint}`);
|
|
421
|
-
this.name = "OxyApiError";
|
|
422
|
-
this.status = opts.status;
|
|
423
|
-
this.code = opts.code ?? null;
|
|
424
|
-
this.hint = opts.hint ?? null;
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
/**
|
|
428
|
-
* Read a non-2xx response from oxy and return an `OxyApiError`.
|
|
429
|
-
* Parses the JSON envelope when present; falls back to raw text
|
|
430
|
-
* (truncated to 240 chars so a runaway HTML error page doesn't
|
|
431
|
-
* dominate the bundle UI).
|
|
432
|
-
*/
|
|
433
|
-
async function apiErrorFromResponse(resp) {
|
|
434
|
-
let body;
|
|
435
|
-
let raw = "";
|
|
436
|
-
try {
|
|
437
|
-
raw = await resp.text();
|
|
438
|
-
body = raw ? JSON.parse(raw) : void 0;
|
|
439
|
-
} catch {}
|
|
440
|
-
if (body && typeof body === "object") {
|
|
441
|
-
const b = body;
|
|
442
|
-
return new OxyApiError({
|
|
443
|
-
status: resp.status,
|
|
444
|
-
message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
|
|
445
|
-
code: typeof b.code === "string" ? b.code : null,
|
|
446
|
-
hint: typeof b.hint === "string" ? b.hint : null
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
|
|
450
|
-
return new OxyApiError({
|
|
451
|
-
status: resp.status,
|
|
452
|
-
message: snippet || `HTTP ${resp.status}`
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
766
|
* Read the resolved manifest from context. Throws if called outside
|
|
457
767
|
* `<OxyAppProvider>` — that's a programmer error worth surfacing
|
|
458
768
|
* loudly, not silently swallowing.
|
|
@@ -506,36 +816,35 @@ function useQuery(input, opts = {}) {
|
|
|
506
816
|
} : s);
|
|
507
817
|
return;
|
|
508
818
|
}
|
|
509
|
-
const ctrl = new AbortController();
|
|
510
819
|
let cancelled = false;
|
|
820
|
+
const cached = getCached(projectId, sqlWithParams, input.database);
|
|
821
|
+
if (cached && nonce === 0) {
|
|
822
|
+
const { columns, rows } = cached;
|
|
823
|
+
const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
|
|
824
|
+
setState({
|
|
825
|
+
rows: objects,
|
|
826
|
+
columns,
|
|
827
|
+
loading: false,
|
|
828
|
+
error: null
|
|
829
|
+
});
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
511
832
|
setState((s) => ({
|
|
512
833
|
...s,
|
|
513
834
|
loading: true,
|
|
514
835
|
error: null
|
|
515
836
|
}));
|
|
516
|
-
|
|
517
|
-
sql: sqlWithParams,
|
|
518
|
-
...input.database ? { database: input.database } : {}
|
|
519
|
-
});
|
|
520
|
-
fetcher(`/api/projects/${projectId}/query`, {
|
|
521
|
-
method: "POST",
|
|
522
|
-
headers: { "content-type": "application/json" },
|
|
523
|
-
body,
|
|
524
|
-
signal: ctrl.signal
|
|
525
|
-
}).then(async (resp) => {
|
|
526
|
-
if (!resp.ok) throw await apiErrorFromResponse(resp);
|
|
527
|
-
return resp.json();
|
|
528
|
-
}).then(({ columns, rows }) => {
|
|
837
|
+
sharedQuery(fetcher, projectId, sqlWithParams, input.database, { force: nonce > 0 }).then(({ columns, rows }) => {
|
|
529
838
|
if (cancelled) return;
|
|
839
|
+
const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
|
|
530
840
|
setState({
|
|
531
|
-
rows:
|
|
841
|
+
rows: objects,
|
|
532
842
|
columns,
|
|
533
843
|
loading: false,
|
|
534
844
|
error: null
|
|
535
845
|
});
|
|
536
846
|
}).catch((err) => {
|
|
537
847
|
if (cancelled) return;
|
|
538
|
-
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
539
848
|
setState((s) => ({
|
|
540
849
|
...s,
|
|
541
850
|
loading: false,
|
|
@@ -544,7 +853,6 @@ function useQuery(input, opts = {}) {
|
|
|
544
853
|
});
|
|
545
854
|
return () => {
|
|
546
855
|
cancelled = true;
|
|
547
|
-
ctrl.abort();
|
|
548
856
|
};
|
|
549
857
|
}, [
|
|
550
858
|
enabled,
|
|
@@ -563,6 +871,81 @@ function useQuery(input, opts = {}) {
|
|
|
563
871
|
};
|
|
564
872
|
}
|
|
565
873
|
/**
|
|
874
|
+
* Imperative hook for invoking an Oxy Function by name.
|
|
875
|
+
*
|
|
876
|
+
* ```tsx
|
|
877
|
+
* const refresh = useFunction("refresh-sales");
|
|
878
|
+
* <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
|
|
879
|
+
* Refresh
|
|
880
|
+
* </button>
|
|
881
|
+
* ```
|
|
882
|
+
*/
|
|
883
|
+
function useFunction(name) {
|
|
884
|
+
const ctx = React.useContext(OxyAppContext);
|
|
885
|
+
if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
|
|
886
|
+
const fetcher = ctx.fetcher;
|
|
887
|
+
const resolved = ctx.resolved;
|
|
888
|
+
const [state, setState] = React.useState({
|
|
889
|
+
data: null,
|
|
890
|
+
isLoading: false,
|
|
891
|
+
error: null,
|
|
892
|
+
logs: []
|
|
893
|
+
});
|
|
894
|
+
return {
|
|
895
|
+
invoke: React.useCallback(async (body, opts) => {
|
|
896
|
+
if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
|
|
897
|
+
const { orgSlug, appSlug, apiBaseUrl } = resolved;
|
|
898
|
+
const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
|
|
899
|
+
setState((s) => ({
|
|
900
|
+
...s,
|
|
901
|
+
isLoading: true,
|
|
902
|
+
error: null
|
|
903
|
+
}));
|
|
904
|
+
try {
|
|
905
|
+
const headers = {
|
|
906
|
+
"content-type": "application/json",
|
|
907
|
+
accept: "text/event-stream"
|
|
908
|
+
};
|
|
909
|
+
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
910
|
+
const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
|
|
911
|
+
const resp = await fetcher(url, {
|
|
912
|
+
method: "POST",
|
|
913
|
+
headers,
|
|
914
|
+
body: JSON.stringify(body ?? {})
|
|
915
|
+
});
|
|
916
|
+
if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
|
|
917
|
+
return readFunctionSseStream(resp);
|
|
918
|
+
});
|
|
919
|
+
setState({
|
|
920
|
+
data: result.value,
|
|
921
|
+
isLoading: false,
|
|
922
|
+
error: null,
|
|
923
|
+
logs: result.logs
|
|
924
|
+
});
|
|
925
|
+
return result.value;
|
|
926
|
+
} catch (err) {
|
|
927
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
928
|
+
const logs = e.logs ?? [];
|
|
929
|
+
setState((s) => ({
|
|
930
|
+
...s,
|
|
931
|
+
isLoading: false,
|
|
932
|
+
error: e,
|
|
933
|
+
logs
|
|
934
|
+
}));
|
|
935
|
+
throw e;
|
|
936
|
+
}
|
|
937
|
+
}, [
|
|
938
|
+
resolved,
|
|
939
|
+
fetcher,
|
|
940
|
+
name
|
|
941
|
+
]),
|
|
942
|
+
data: state.data,
|
|
943
|
+
isLoading: state.isLoading,
|
|
944
|
+
error: state.error,
|
|
945
|
+
logs: state.logs
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
566
949
|
* Run a semantic-layer query against the project's `.view.yml` /
|
|
567
950
|
* `.topic.yml` definitions. The server compiles to SQL and executes
|
|
568
951
|
* through the same connector path as `useQuery`, so result shape
|
|
@@ -610,7 +993,8 @@ function useSemanticQuery(input, opts = {}) {
|
|
|
610
993
|
filters: input.filters ?? [],
|
|
611
994
|
...input.limit != null ? { limit: input.limit } : {}
|
|
612
995
|
});
|
|
613
|
-
|
|
996
|
+
const url = `/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`;
|
|
997
|
+
fetcher(url, {
|
|
614
998
|
method: "POST",
|
|
615
999
|
headers: { "content-type": "application/json" },
|
|
616
1000
|
body,
|
|
@@ -620,8 +1004,9 @@ function useSemanticQuery(input, opts = {}) {
|
|
|
620
1004
|
return resp.json();
|
|
621
1005
|
}).then(({ columns, rows, truncated, sql }) => {
|
|
622
1006
|
if (cancelled) return;
|
|
1007
|
+
const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
|
|
623
1008
|
setState({
|
|
624
|
-
rows:
|
|
1009
|
+
rows: objects,
|
|
625
1010
|
columns,
|
|
626
1011
|
truncated,
|
|
627
1012
|
sql: sql ?? null,
|
|
@@ -1008,7 +1393,7 @@ function parseSseData(raw) {
|
|
|
1008
1393
|
* uniformly. New types added upstream don't surface as artifacts
|
|
1009
1394
|
* until added here — that's intentional, the renderer needs to
|
|
1010
1395
|
* know how to display each. */
|
|
1011
|
-
const SQL_EVENT_TYPES = new Set([
|
|
1396
|
+
const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1012
1397
|
"query_executed",
|
|
1013
1398
|
"query_generated",
|
|
1014
1399
|
"verified_sql",
|
|
@@ -1137,6 +1522,82 @@ function sleep(ms, signal) {
|
|
|
1137
1522
|
});
|
|
1138
1523
|
}
|
|
1139
1524
|
/**
|
|
1525
|
+
* Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
|
|
1526
|
+
* `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
|
|
1527
|
+
* (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
|
|
1528
|
+
* grouped by name, with drill-down into recent occurrences.
|
|
1529
|
+
*
|
|
1530
|
+
* The handler returned by [`useTrackEvent`] is **fire-and-forget**:
|
|
1531
|
+
* it enqueues the event into an in-memory batch flushed every second
|
|
1532
|
+
* (and on `pagehide` so a navigation away doesn't drop the tail).
|
|
1533
|
+
* No await semantics — call it inline from a click handler without
|
|
1534
|
+
* awaiting it. Server-side validation errors are logged to the
|
|
1535
|
+
* console; the call site doesn't need to handle them.
|
|
1536
|
+
*
|
|
1537
|
+
* Example:
|
|
1538
|
+
* ```tsx
|
|
1539
|
+
* const track = useTrackEvent();
|
|
1540
|
+
* <button
|
|
1541
|
+
* onClick={() => {
|
|
1542
|
+
* track("export-clicked", { format: "csv", rowCount });
|
|
1543
|
+
* doExport();
|
|
1544
|
+
* }}
|
|
1545
|
+
* >Export</button>
|
|
1546
|
+
* ```
|
|
1547
|
+
*
|
|
1548
|
+
* Rate-limited at 60/min per (user, app) on the server. A burst that
|
|
1549
|
+
* trips the limit drops the excess events with a console warning;
|
|
1550
|
+
* within-limit events are unaffected.
|
|
1551
|
+
*/
|
|
1552
|
+
function useTrackEvent() {
|
|
1553
|
+
const { projectId, fetcher } = useOxyApp();
|
|
1554
|
+
const queueRef = React.useRef([]);
|
|
1555
|
+
const flushTimerRef = React.useRef(null);
|
|
1556
|
+
const flush = React.useCallback(() => {
|
|
1557
|
+
flushTimerRef.current = null;
|
|
1558
|
+
if (!projectId) return;
|
|
1559
|
+
const batch = queueRef.current;
|
|
1560
|
+
if (batch.length === 0) return;
|
|
1561
|
+
queueRef.current = [];
|
|
1562
|
+
for (const evt of batch) {
|
|
1563
|
+
const url = `/api/customer-apps/${projectId}/events`;
|
|
1564
|
+
const body = JSON.stringify(evt);
|
|
1565
|
+
try {
|
|
1566
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
1567
|
+
else fetcher(url, {
|
|
1568
|
+
method: "POST",
|
|
1569
|
+
headers: { "content-type": "application/json" },
|
|
1570
|
+
body
|
|
1571
|
+
}).catch((e) => {
|
|
1572
|
+
console.warn("[oxy] useTrackEvent flush failed:", e);
|
|
1573
|
+
});
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
console.warn("[oxy] useTrackEvent enqueue failed:", e);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}, [projectId, fetcher]);
|
|
1579
|
+
React.useEffect(() => {
|
|
1580
|
+
if (typeof window === "undefined") return;
|
|
1581
|
+
const onHide = () => flush();
|
|
1582
|
+
window.addEventListener("pagehide", onHide);
|
|
1583
|
+
return () => {
|
|
1584
|
+
window.removeEventListener("pagehide", onHide);
|
|
1585
|
+
flush();
|
|
1586
|
+
if (flushTimerRef.current !== null) {
|
|
1587
|
+
clearTimeout(flushTimerRef.current);
|
|
1588
|
+
flushTimerRef.current = null;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}, [flush]);
|
|
1592
|
+
return React.useCallback((name, payload) => {
|
|
1593
|
+
queueRef.current.push({
|
|
1594
|
+
event_name: name,
|
|
1595
|
+
payload: payload ?? {}
|
|
1596
|
+
});
|
|
1597
|
+
if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
|
|
1598
|
+
}, [flush]);
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1140
1601
|
* Renders an agent run's answer + artifacts + thread link as a
|
|
1141
1602
|
* single block. The default styling is intentionally neutral
|
|
1142
1603
|
* (system fonts, gray surfaces) so it blends into any bundle.
|
|
@@ -1158,25 +1619,55 @@ function OxyAnswer(props) {
|
|
|
1158
1619
|
const isRunning = state === "running";
|
|
1159
1620
|
const isFailed = state === "failed";
|
|
1160
1621
|
const needsClarification = state === "needs_clarification";
|
|
1161
|
-
return /* @__PURE__ */
|
|
1622
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1162
1623
|
className,
|
|
1163
|
-
style: styles.answerWrap
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1624
|
+
style: styles.answerWrap,
|
|
1625
|
+
children: [
|
|
1626
|
+
isRunning && answer === null ? /* @__PURE__ */ jsxs("div", {
|
|
1627
|
+
style: styles.statusRow,
|
|
1628
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1629
|
+
style: styles.spinner,
|
|
1630
|
+
"aria-hidden": "true"
|
|
1631
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1632
|
+
style: styles.statusText,
|
|
1633
|
+
children: "Thinking…"
|
|
1634
|
+
})]
|
|
1635
|
+
}) : null,
|
|
1636
|
+
artifacts.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
1637
|
+
style: styles.artifactList,
|
|
1638
|
+
children: artifacts.map((a) => /* @__PURE__ */ jsx(SqlArtifactBlock, {
|
|
1639
|
+
artifact: a,
|
|
1640
|
+
maxRows: maxArtifactRows
|
|
1641
|
+
}, a.id))
|
|
1642
|
+
}) : null,
|
|
1643
|
+
answer ? /* @__PURE__ */ jsx("div", {
|
|
1644
|
+
style: styles.markdown,
|
|
1645
|
+
children: /* @__PURE__ */ jsx(MarkdownText, { text: answer })
|
|
1646
|
+
}) : null,
|
|
1647
|
+
needsClarification && clarification ? /* @__PURE__ */ jsxs("div", {
|
|
1648
|
+
style: styles.clarification,
|
|
1649
|
+
children: [/* @__PURE__ */ jsx("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ jsx("div", {
|
|
1650
|
+
style: { marginTop: 4 },
|
|
1651
|
+
children: clarification
|
|
1652
|
+
})]
|
|
1653
|
+
}) : null,
|
|
1654
|
+
isFailed && error ? /* @__PURE__ */ jsx(ErrorBlock, { error }) : null,
|
|
1655
|
+
threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ jsxs("div", {
|
|
1656
|
+
style: styles.threadLinkRow,
|
|
1657
|
+
children: [/* @__PURE__ */ jsxs("a", {
|
|
1658
|
+
href: threadUrl,
|
|
1659
|
+
target: "_blank",
|
|
1660
|
+
rel: "noreferrer noopener",
|
|
1661
|
+
style: styles.threadLink,
|
|
1662
|
+
children: [threadLinkLabel, " →"]
|
|
1663
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1664
|
+
style: styles.betaBadge,
|
|
1665
|
+
title: "Thread linking is in beta — see docs",
|
|
1666
|
+
children: "beta"
|
|
1667
|
+
})]
|
|
1668
|
+
}) : null
|
|
1669
|
+
]
|
|
1670
|
+
});
|
|
1180
1671
|
}
|
|
1181
1672
|
/**
|
|
1182
1673
|
* Complete drop-in chat surface. One agent, one input, one answer
|
|
@@ -1200,37 +1691,48 @@ function OxyChat(props) {
|
|
|
1200
1691
|
if (!q || run.state === "running") return;
|
|
1201
1692
|
run.ask(q);
|
|
1202
1693
|
}, [question, run]);
|
|
1203
|
-
return /* @__PURE__ */
|
|
1694
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1204
1695
|
className,
|
|
1205
|
-
style: styles.chatWrap
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1696
|
+
style: styles.chatWrap,
|
|
1697
|
+
children: [/* @__PURE__ */ jsxs("form", {
|
|
1698
|
+
onSubmit: submit,
|
|
1699
|
+
style: styles.chatForm,
|
|
1700
|
+
children: [
|
|
1701
|
+
/* @__PURE__ */ jsx("input", {
|
|
1702
|
+
type: "text",
|
|
1703
|
+
value: question,
|
|
1704
|
+
onChange: (e) => setQuestion(e.target.value),
|
|
1705
|
+
placeholder,
|
|
1706
|
+
disabled: run.state === "running",
|
|
1707
|
+
style: styles.chatInput,
|
|
1708
|
+
"aria-label": "Question"
|
|
1709
|
+
}),
|
|
1710
|
+
/* @__PURE__ */ jsx("button", {
|
|
1711
|
+
type: "submit",
|
|
1712
|
+
disabled: run.state === "running" || question.trim() === "",
|
|
1713
|
+
style: styles.chatSubmit,
|
|
1714
|
+
children: run.state === "running" ? "…" : submitLabel
|
|
1715
|
+
}),
|
|
1716
|
+
run.state === "running" ? /* @__PURE__ */ jsx("button", {
|
|
1717
|
+
type: "button",
|
|
1718
|
+
onClick: run.cancel,
|
|
1719
|
+
style: styles.chatCancel,
|
|
1720
|
+
children: "Stop"
|
|
1721
|
+
}) : null
|
|
1722
|
+
]
|
|
1723
|
+
}), run.state === "idle" ? emptyState ?? /* @__PURE__ */ jsx("div", {
|
|
1724
|
+
style: styles.emptyState,
|
|
1725
|
+
children: "Ask a question to get started."
|
|
1726
|
+
}) : /* @__PURE__ */ jsx(OxyAnswer, {
|
|
1727
|
+
answer: run.answer,
|
|
1728
|
+
artifacts: run.artifacts,
|
|
1729
|
+
state: run.state,
|
|
1730
|
+
clarification: run.clarification,
|
|
1731
|
+
error: run.error,
|
|
1732
|
+
threadUrl: run.threadUrl,
|
|
1733
|
+
maxArtifactRows
|
|
1734
|
+
})]
|
|
1735
|
+
});
|
|
1234
1736
|
}
|
|
1235
1737
|
function SqlArtifactBlock(props) {
|
|
1236
1738
|
const { artifact, maxRows } = props;
|
|
@@ -1239,17 +1741,53 @@ function SqlArtifactBlock(props) {
|
|
|
1239
1741
|
const truncated = results ? results.rows.length > maxRows : false;
|
|
1240
1742
|
const visibleRows = results ? results.rows.slice(0, maxRows) : [];
|
|
1241
1743
|
const sourceLabel = artifact.source === "verified_sql" ? "Verified query" : artifact.source === "semantic_query" ? "Semantic query" : artifact.source === "omni_query" ? "Omni query" : "Query";
|
|
1242
|
-
return /* @__PURE__ */
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1744
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1745
|
+
style: styles.artifact,
|
|
1746
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
1747
|
+
type: "button",
|
|
1748
|
+
onClick: () => setOpen((o) => !o),
|
|
1749
|
+
style: styles.artifactHeader,
|
|
1750
|
+
children: [
|
|
1751
|
+
/* @__PURE__ */ jsx("span", {
|
|
1752
|
+
style: styles.artifactBadge,
|
|
1753
|
+
children: sourceLabel
|
|
1754
|
+
}),
|
|
1755
|
+
/* @__PURE__ */ jsx("span", {
|
|
1756
|
+
style: styles.artifactSummary,
|
|
1757
|
+
children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
|
|
1758
|
+
}),
|
|
1759
|
+
/* @__PURE__ */ jsx("span", {
|
|
1760
|
+
style: styles.artifactToggle,
|
|
1761
|
+
children: open ? "Hide" : "Show"
|
|
1762
|
+
})
|
|
1763
|
+
]
|
|
1764
|
+
}), open ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("pre", {
|
|
1765
|
+
style: styles.sqlBlock,
|
|
1766
|
+
children: artifact.sql
|
|
1767
|
+
}), artifact.error ? /* @__PURE__ */ jsx("div", {
|
|
1768
|
+
style: styles.error,
|
|
1769
|
+
children: artifact.error
|
|
1770
|
+
}) : results ? /* @__PURE__ */ jsxs("div", {
|
|
1771
|
+
style: styles.resultsWrap,
|
|
1772
|
+
children: [/* @__PURE__ */ jsxs("table", {
|
|
1773
|
+
style: styles.resultsTable,
|
|
1774
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: results.columns.map((c) => /* @__PURE__ */ jsx("th", {
|
|
1775
|
+
style: styles.resultsTh,
|
|
1776
|
+
children: c
|
|
1777
|
+
}, c)) }) }), /* @__PURE__ */ jsx("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx("td", {
|
|
1778
|
+
style: styles.resultsTd,
|
|
1779
|
+
children: formatCell(cell)
|
|
1780
|
+
}, j)) }, i)) })]
|
|
1781
|
+
}), truncated ? /* @__PURE__ */ jsxs("div", {
|
|
1782
|
+
style: styles.truncatedNote,
|
|
1783
|
+
children: [
|
|
1784
|
+
"+",
|
|
1785
|
+
results.rows.length - maxRows,
|
|
1786
|
+
" more rows. Open the thread in Oxy to see all."
|
|
1787
|
+
]
|
|
1788
|
+
}) : null]
|
|
1789
|
+
}) : null] }) : null]
|
|
1790
|
+
});
|
|
1253
1791
|
}
|
|
1254
1792
|
function formatCell(value) {
|
|
1255
1793
|
if (value === null || value === void 0) return "—";
|
|
@@ -1264,16 +1802,36 @@ function formatCell(value) {
|
|
|
1264
1802
|
*/
|
|
1265
1803
|
function ErrorBlock(props) {
|
|
1266
1804
|
const { error } = props;
|
|
1267
|
-
if (error instanceof OxyApiError) return /* @__PURE__ */
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1805
|
+
if (error instanceof OxyApiError) return /* @__PURE__ */ jsxs("div", {
|
|
1806
|
+
style: styles.error,
|
|
1807
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
1808
|
+
/* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
|
|
1809
|
+
" ",
|
|
1810
|
+
error.message.split("\n\n")[0]
|
|
1811
|
+
] }), error.hint ? /* @__PURE__ */ jsxs("div", {
|
|
1812
|
+
style: {
|
|
1813
|
+
marginTop: 6,
|
|
1814
|
+
fontWeight: 400,
|
|
1815
|
+
whiteSpace: "pre-wrap"
|
|
1816
|
+
},
|
|
1817
|
+
children: [
|
|
1818
|
+
/* @__PURE__ */ jsx("strong", { children: "Hint:" }),
|
|
1819
|
+
" ",
|
|
1820
|
+
error.hint
|
|
1821
|
+
]
|
|
1822
|
+
}) : null]
|
|
1823
|
+
});
|
|
1824
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1825
|
+
style: styles.error,
|
|
1826
|
+
children: [
|
|
1827
|
+
/* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
|
|
1828
|
+
" ",
|
|
1829
|
+
error.message
|
|
1830
|
+
]
|
|
1831
|
+
});
|
|
1273
1832
|
}
|
|
1274
1833
|
function MarkdownText(props) {
|
|
1275
|
-
|
|
1276
|
-
return /* @__PURE__ */ React.createElement(React.Fragment, null, blocks);
|
|
1834
|
+
return /* @__PURE__ */ jsx(Fragment, { children: React.useMemo(() => parseMarkdown(props.text), [props.text]) });
|
|
1277
1835
|
}
|
|
1278
1836
|
function parseMarkdown(text) {
|
|
1279
1837
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
@@ -1343,27 +1901,23 @@ function parseMarkdown(text) {
|
|
|
1343
1901
|
}
|
|
1344
1902
|
return blocks.map((b, idx) => {
|
|
1345
1903
|
switch (b.kind) {
|
|
1346
|
-
case "h": {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
style: headingStyle
|
|
1352
|
-
}, renderInline(b.text));
|
|
1353
|
-
}
|
|
1354
|
-
case "code": return /* @__PURE__ */ React.createElement("pre", {
|
|
1355
|
-
key: idx,
|
|
1904
|
+
case "h": return /* @__PURE__ */ jsx(`h${b.level}`, {
|
|
1905
|
+
style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
|
|
1906
|
+
children: renderInline(b.text)
|
|
1907
|
+
}, idx);
|
|
1908
|
+
case "code": return /* @__PURE__ */ jsx("pre", {
|
|
1356
1909
|
style: styles.codeBlock,
|
|
1357
|
-
"data-lang": b.lang || void 0
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
style: styles.list
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
style: styles.paragraph
|
|
1366
|
-
|
|
1910
|
+
"data-lang": b.lang || void 0,
|
|
1911
|
+
children: /* @__PURE__ */ jsx("code", { children: b.code })
|
|
1912
|
+
}, idx);
|
|
1913
|
+
case "list": return /* @__PURE__ */ jsx("ul", {
|
|
1914
|
+
style: styles.list,
|
|
1915
|
+
children: b.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: renderInline(item) }, i))
|
|
1916
|
+
}, idx);
|
|
1917
|
+
case "p": return /* @__PURE__ */ jsx("p", {
|
|
1918
|
+
style: styles.paragraph,
|
|
1919
|
+
children: renderInline(b.text)
|
|
1920
|
+
}, idx);
|
|
1367
1921
|
}
|
|
1368
1922
|
});
|
|
1369
1923
|
}
|
|
@@ -1379,27 +1933,31 @@ function renderInline(text) {
|
|
|
1379
1933
|
const patterns = [
|
|
1380
1934
|
{
|
|
1381
1935
|
re: /`([^`]+)`/,
|
|
1382
|
-
render: (m) => /* @__PURE__ */
|
|
1936
|
+
render: (m) => /* @__PURE__ */ jsx("code", {
|
|
1937
|
+
style: styles.inlineCode,
|
|
1938
|
+
children: m[1]
|
|
1939
|
+
})
|
|
1383
1940
|
},
|
|
1384
1941
|
{
|
|
1385
1942
|
re: /\[([^\]]+)\]\(([^)]+)\)/,
|
|
1386
1943
|
render: (m) => {
|
|
1387
|
-
if (isSafeLinkHref(m[2])) return /* @__PURE__ */
|
|
1944
|
+
if (isSafeLinkHref(m[2])) return /* @__PURE__ */ jsx("a", {
|
|
1388
1945
|
href: m[2],
|
|
1389
1946
|
target: "_blank",
|
|
1390
1947
|
rel: "noreferrer noopener",
|
|
1391
|
-
style: styles.link
|
|
1392
|
-
|
|
1393
|
-
|
|
1948
|
+
style: styles.link,
|
|
1949
|
+
children: m[1]
|
|
1950
|
+
});
|
|
1951
|
+
return /* @__PURE__ */ jsx(Fragment, { children: m[1] });
|
|
1394
1952
|
}
|
|
1395
1953
|
},
|
|
1396
1954
|
{
|
|
1397
1955
|
re: /\*\*([^*]+)\*\*/,
|
|
1398
|
-
render: (m) => /* @__PURE__ */
|
|
1956
|
+
render: (m) => /* @__PURE__ */ jsx("strong", { children: m[1] })
|
|
1399
1957
|
},
|
|
1400
1958
|
{
|
|
1401
1959
|
re: /\*([^*]+)\*/,
|
|
1402
|
-
render: (m) => /* @__PURE__ */
|
|
1960
|
+
render: (m) => /* @__PURE__ */ jsx("em", { children: m[1] })
|
|
1403
1961
|
}
|
|
1404
1962
|
];
|
|
1405
1963
|
while (remaining.length > 0) {
|
|
@@ -1417,7 +1975,7 @@ function renderInline(text) {
|
|
|
1417
1975
|
break;
|
|
1418
1976
|
}
|
|
1419
1977
|
if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
|
|
1420
|
-
segments.push(/* @__PURE__ */
|
|
1978
|
+
segments.push(/* @__PURE__ */ jsx(React.Fragment, { children: earliest.node }, key++));
|
|
1421
1979
|
remaining = remaining.slice(earliest.idx + earliest.len);
|
|
1422
1980
|
}
|
|
1423
1981
|
return segments;
|
|
@@ -1647,5 +2205,133 @@ const styles = {
|
|
|
1647
2205
|
};
|
|
1648
2206
|
|
|
1649
2207
|
//#endregion
|
|
1650
|
-
|
|
2208
|
+
//#region src/metricTree.ts
|
|
2209
|
+
/**
|
|
2210
|
+
* Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
|
|
2211
|
+
* airlayer metric-tree analyses (tree introspection, sensitivity, predict,
|
|
2212
|
+
* explain, opportunity) over typed methods.
|
|
2213
|
+
*
|
|
2214
|
+
* Construction is internal to {@link OxyClient} — call `client.metricTree`
|
|
2215
|
+
* to access an instance rather than building one yourself.
|
|
2216
|
+
*
|
|
2217
|
+
* @example
|
|
2218
|
+
* ```typescript
|
|
2219
|
+
* const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
|
|
2220
|
+
* const tree = await client.metricTree.getTree();
|
|
2221
|
+
* const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2222
|
+
* ```
|
|
2223
|
+
*/
|
|
2224
|
+
var MetricTreeClient = class {
|
|
2225
|
+
constructor(config, request) {
|
|
2226
|
+
this.config = config;
|
|
2227
|
+
this.request = request;
|
|
2228
|
+
}
|
|
2229
|
+
path(suffix) {
|
|
2230
|
+
return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
|
|
2231
|
+
}
|
|
2232
|
+
buildQuery(extra = {}) {
|
|
2233
|
+
const params = { ...extra };
|
|
2234
|
+
if (this.config.branch) params.branch = this.config.branch;
|
|
2235
|
+
const qs = new URLSearchParams(params).toString();
|
|
2236
|
+
return qs ? `?${qs}` : "";
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Fetch the full metric tree, or the subtree rooted at `root`.
|
|
2240
|
+
*
|
|
2241
|
+
* @param root - Optional fully-qualified measure id to root the tree at.
|
|
2242
|
+
* @returns Nodes (measures) and edges (component / driver relationships).
|
|
2243
|
+
*
|
|
2244
|
+
* @example
|
|
2245
|
+
* ```typescript
|
|
2246
|
+
* const tree = await client.metricTree.getTree();
|
|
2247
|
+
* const subtree = await client.metricTree.getTree("orders.net_revenue");
|
|
2248
|
+
* ```
|
|
2249
|
+
*/
|
|
2250
|
+
async getTree(root) {
|
|
2251
|
+
const query = this.buildQuery(root ? { root } : {});
|
|
2252
|
+
return this.request(this.path(query));
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Rank the declared drivers of a measure by influence.
|
|
2256
|
+
*
|
|
2257
|
+
* @param measureId - Fully-qualified measure id (`view.measure`).
|
|
2258
|
+
*
|
|
2259
|
+
* @example
|
|
2260
|
+
* ```typescript
|
|
2261
|
+
* const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2262
|
+
* for (const driver of sensitivity.drivers) {
|
|
2263
|
+
* console.log(driver.measure, driver.direction, driver.strength);
|
|
2264
|
+
* }
|
|
2265
|
+
* ```
|
|
2266
|
+
*/
|
|
2267
|
+
async getSensitivity(measureId) {
|
|
2268
|
+
const query = this.buildQuery();
|
|
2269
|
+
return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Propagate hypothetical `(measure, delta)` changes upward through the
|
|
2273
|
+
* tree. Returns the estimated impact on every downstream measure.
|
|
2274
|
+
*
|
|
2275
|
+
* @example
|
|
2276
|
+
* ```typescript
|
|
2277
|
+
* const result = await client.metricTree.predict([
|
|
2278
|
+
* { measure: "marketing_spend.total_spend", delta: 10000 },
|
|
2279
|
+
* ]);
|
|
2280
|
+
* ```
|
|
2281
|
+
*/
|
|
2282
|
+
async predict(changes) {
|
|
2283
|
+
const query = this.buildQuery();
|
|
2284
|
+
return this.request(this.path(`/predict${query}`), {
|
|
2285
|
+
method: "POST",
|
|
2286
|
+
body: JSON.stringify({ changes })
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Period-over-period root-cause decomposition. Recursively splits the
|
|
2291
|
+
* target measure by components and dimensions until the move concentrates.
|
|
2292
|
+
*
|
|
2293
|
+
* @example
|
|
2294
|
+
* ```typescript
|
|
2295
|
+
* const result = await client.metricTree.explain({
|
|
2296
|
+
* target: "financials.operating_profit",
|
|
2297
|
+
* time_dimension: "financials.month",
|
|
2298
|
+
* current_period: ["2025-09-01", "2025-09-30"],
|
|
2299
|
+
* previous_period: ["2025-08-01", "2025-08-31"],
|
|
2300
|
+
* });
|
|
2301
|
+
* ```
|
|
2302
|
+
*/
|
|
2303
|
+
async explain(request) {
|
|
2304
|
+
const query = this.buildQuery();
|
|
2305
|
+
return this.request(this.path(`/explain${query}`), {
|
|
2306
|
+
method: "POST",
|
|
2307
|
+
body: JSON.stringify(request)
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Size the upside opportunity for a measure by finding underperforming
|
|
2312
|
+
* segments. Skips high-cardinality dimensions and trims the long tail.
|
|
2313
|
+
*
|
|
2314
|
+
* @example
|
|
2315
|
+
* ```typescript
|
|
2316
|
+
* const result = await client.metricTree.findOpportunities({
|
|
2317
|
+
* target: "orders.net_revenue",
|
|
2318
|
+
* time_dimension: "orders.order_date",
|
|
2319
|
+
* period: ["2025-09-01", "2025-09-30"],
|
|
2320
|
+
* });
|
|
2321
|
+
* for (const dim of result.dimensions) {
|
|
2322
|
+
* console.log(dim.dimension, "+", dim.total_upside);
|
|
2323
|
+
* }
|
|
2324
|
+
* ```
|
|
2325
|
+
*/
|
|
2326
|
+
async findOpportunities(request) {
|
|
2327
|
+
const query = this.buildQuery();
|
|
2328
|
+
return this.request(this.path(`/opportunity${query}`), {
|
|
2329
|
+
method: "POST",
|
|
2330
|
+
body: JSON.stringify(request)
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
//#endregion
|
|
2336
|
+
export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, apiErrorFromResponse, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
|
|
1651
2337
|
//# sourceMappingURL=index.mjs.map
|