@oxy-hq/sdk 2.0.1 → 2.1.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/index.cjs +749 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +556 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +556 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +745 -115
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
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. */
|
|
@@ -255,13 +337,156 @@ function validateManifest(raw, url) {
|
|
|
255
337
|
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
256
338
|
slug: raw.slug,
|
|
257
339
|
orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
|
|
258
|
-
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
|
|
340
|
+
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
|
|
341
|
+
functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
|
|
259
342
|
};
|
|
260
343
|
}
|
|
344
|
+
const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
345
|
+
/**
|
|
346
|
+
* Validate the optional `functions` map. Each key is a function name;
|
|
347
|
+
* each value declares how the function is invoked. Mirrors the
|
|
348
|
+
* server-side validation in `customer_apps_publish.rs` so a bad
|
|
349
|
+
* manifest fails at build, not at publish.
|
|
350
|
+
*/
|
|
351
|
+
function validateFunctions(raw) {
|
|
352
|
+
if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
|
|
353
|
+
const out = {};
|
|
354
|
+
for (const [fnName, value] of Object.entries(raw)) {
|
|
355
|
+
if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
|
|
356
|
+
if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
|
|
357
|
+
const fn = {};
|
|
358
|
+
if (value.entry !== void 0) {
|
|
359
|
+
if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
|
|
360
|
+
fn.entry = value.entry;
|
|
361
|
+
}
|
|
362
|
+
if (value.schedule !== void 0) {
|
|
363
|
+
if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
|
|
364
|
+
fn.schedule = value.schedule;
|
|
365
|
+
}
|
|
366
|
+
if (value.timezone !== void 0) {
|
|
367
|
+
if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
|
|
368
|
+
fn.timezone = value.timezone;
|
|
369
|
+
}
|
|
370
|
+
if (value.route !== void 0) {
|
|
371
|
+
if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
|
|
372
|
+
fn.route = value.route;
|
|
373
|
+
}
|
|
374
|
+
if (value.airwayStep !== void 0) {
|
|
375
|
+
const step = value.airwayStep;
|
|
376
|
+
if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
|
|
377
|
+
fn.airwayStep = {
|
|
378
|
+
pipeline: step.pipeline,
|
|
379
|
+
resource: step.resource
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (value.timeoutSeconds !== void 0) {
|
|
383
|
+
const t = value.timeoutSeconds;
|
|
384
|
+
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]`);
|
|
385
|
+
fn.timeoutSeconds = t;
|
|
386
|
+
}
|
|
387
|
+
if (value.cache !== void 0) {
|
|
388
|
+
const c = value.cache;
|
|
389
|
+
if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
|
|
390
|
+
if (c.ttlSeconds !== void 0) {
|
|
391
|
+
const ttl = c.ttlSeconds;
|
|
392
|
+
if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
|
|
393
|
+
fn.cache = { ttlSeconds: ttl };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const hasSchedule = fn.schedule !== void 0;
|
|
397
|
+
const hasAirway = fn.airwayStep !== void 0;
|
|
398
|
+
if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
|
|
399
|
+
out[fnName] = fn;
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
261
403
|
function isRecord(v) {
|
|
262
404
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
263
405
|
}
|
|
264
406
|
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/customer-app/function-invoke.ts
|
|
409
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
410
|
+
/**
|
|
411
|
+
* Dedup key for an invocation: function name + its (stable-serialized) body,
|
|
412
|
+
* joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
|
|
413
|
+
* the separator can never collide with a name.
|
|
414
|
+
*/
|
|
415
|
+
function functionInvokeKey(name, body) {
|
|
416
|
+
return `${name}\n${JSON.stringify(body ?? {})}`;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Run `run()` unless an identical invocation is already in flight, in which
|
|
420
|
+
* case share its promise. The entry is removed once the promise settles — so
|
|
421
|
+
* this dedups concurrency only, it does NOT memoize the result.
|
|
422
|
+
*/
|
|
423
|
+
function sharedFunctionInvoke(key, run) {
|
|
424
|
+
const existing = inflight.get(key);
|
|
425
|
+
if (existing) return existing;
|
|
426
|
+
const p = run().finally(() => {
|
|
427
|
+
inflight.delete(key);
|
|
428
|
+
});
|
|
429
|
+
inflight.set(key, p);
|
|
430
|
+
return p;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/customer-app/function-sse.ts
|
|
435
|
+
/**
|
|
436
|
+
* Read a `text/event-stream` function response to completion. Resolves with the
|
|
437
|
+
* decoded result + captured logs, or rejects (with `.logs` attached) on an
|
|
438
|
+
* `event: error` frame / a stream that ends without a terminal event.
|
|
439
|
+
*/
|
|
440
|
+
async function readFunctionSseStream(resp) {
|
|
441
|
+
const reader = resp.body?.getReader();
|
|
442
|
+
if (!reader) throw new Error("function response has no body stream");
|
|
443
|
+
const decoder = new TextDecoder();
|
|
444
|
+
let buffer = "";
|
|
445
|
+
let dataPayload = "";
|
|
446
|
+
const logs = [];
|
|
447
|
+
const handleFrame = (frame) => {
|
|
448
|
+
let event = "message";
|
|
449
|
+
let data = "";
|
|
450
|
+
for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
451
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
452
|
+
if (event === "log") try {
|
|
453
|
+
const l = JSON.parse(data);
|
|
454
|
+
logs.push({
|
|
455
|
+
level: String(l.level ?? "info"),
|
|
456
|
+
message: String(l.message ?? "")
|
|
457
|
+
});
|
|
458
|
+
} catch {}
|
|
459
|
+
else if (event === "data") dataPayload = data;
|
|
460
|
+
else if (event === "done") return {
|
|
461
|
+
done: true,
|
|
462
|
+
value: dataPayload ? JSON.parse(dataPayload) : null
|
|
463
|
+
};
|
|
464
|
+
else if (event === "error") {
|
|
465
|
+
const payload = data ? JSON.parse(data) : {};
|
|
466
|
+
const err = new Error(payload.message || payload.error || "function invocation failed");
|
|
467
|
+
err.name = payload.error || "FunctionError";
|
|
468
|
+
err.logs = logs;
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
for (;;) {
|
|
473
|
+
const { done, value } = await reader.read();
|
|
474
|
+
if (done) break;
|
|
475
|
+
buffer += decoder.decode(value, { stream: true });
|
|
476
|
+
let sep;
|
|
477
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
478
|
+
const frame = buffer.slice(0, sep);
|
|
479
|
+
buffer = buffer.slice(sep + 2);
|
|
480
|
+
const result = handleFrame(frame);
|
|
481
|
+
if (result) return {
|
|
482
|
+
value: result.value,
|
|
483
|
+
logs
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
throw new Error("function stream ended without a terminal event");
|
|
488
|
+
}
|
|
489
|
+
|
|
265
490
|
//#endregion
|
|
266
491
|
//#region src/customer-app/interpolate.ts
|
|
267
492
|
/**
|
|
@@ -367,26 +592,55 @@ function OxyAppProvider(props) {
|
|
|
367
592
|
}, [manifestOptions, fetcher]);
|
|
368
593
|
if (state.status === "error" && state.error) {
|
|
369
594
|
const err = state.error;
|
|
370
|
-
return /* @__PURE__ */
|
|
595
|
+
return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
596
|
+
value: state,
|
|
597
|
+
children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
|
|
598
|
+
});
|
|
371
599
|
}
|
|
372
|
-
if (state.status === "loading") return /* @__PURE__ */
|
|
373
|
-
|
|
600
|
+
if (state.status === "loading") return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
601
|
+
value: state,
|
|
602
|
+
children: fallback ?? null
|
|
603
|
+
});
|
|
604
|
+
return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
605
|
+
value: state,
|
|
606
|
+
children
|
|
607
|
+
});
|
|
374
608
|
}
|
|
375
609
|
function defaultErrorFallback(err) {
|
|
376
|
-
return /* @__PURE__ */
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
610
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
611
|
+
style: {
|
|
612
|
+
margin: "2rem auto",
|
|
613
|
+
maxWidth: "640px",
|
|
614
|
+
padding: "1rem",
|
|
615
|
+
border: "1px solid #fca5a5",
|
|
616
|
+
background: "#fee2e2",
|
|
617
|
+
color: "#991b1b",
|
|
618
|
+
borderRadius: "8px",
|
|
619
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
620
|
+
fontSize: "14px"
|
|
621
|
+
},
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ jsx("div", {
|
|
624
|
+
style: { fontWeight: 600 },
|
|
625
|
+
children: err.title
|
|
626
|
+
}),
|
|
627
|
+
/* @__PURE__ */ jsx("pre", {
|
|
628
|
+
style: {
|
|
629
|
+
fontSize: "12px",
|
|
630
|
+
marginTop: "4px"
|
|
631
|
+
},
|
|
632
|
+
children: err.message
|
|
633
|
+
}),
|
|
634
|
+
/* @__PURE__ */ jsxs("div", {
|
|
635
|
+
style: { marginTop: "12px" },
|
|
636
|
+
children: [
|
|
637
|
+
/* @__PURE__ */ jsx("strong", { children: "What to try:" }),
|
|
638
|
+
" ",
|
|
639
|
+
err.hint
|
|
640
|
+
]
|
|
641
|
+
})
|
|
642
|
+
]
|
|
643
|
+
});
|
|
390
644
|
}
|
|
391
645
|
/**
|
|
392
646
|
* Emit a one-time `console.warn` the first time a beta hook is
|
|
@@ -563,6 +817,81 @@ function useQuery(input, opts = {}) {
|
|
|
563
817
|
};
|
|
564
818
|
}
|
|
565
819
|
/**
|
|
820
|
+
* Imperative hook for invoking an Oxy Function by name.
|
|
821
|
+
*
|
|
822
|
+
* ```tsx
|
|
823
|
+
* const refresh = useFunction("refresh-sales");
|
|
824
|
+
* <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
|
|
825
|
+
* Refresh
|
|
826
|
+
* </button>
|
|
827
|
+
* ```
|
|
828
|
+
*/
|
|
829
|
+
function useFunction(name) {
|
|
830
|
+
const ctx = React.useContext(OxyAppContext);
|
|
831
|
+
if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
|
|
832
|
+
const fetcher = ctx.fetcher;
|
|
833
|
+
const resolved = ctx.resolved;
|
|
834
|
+
const [state, setState] = React.useState({
|
|
835
|
+
data: null,
|
|
836
|
+
isLoading: false,
|
|
837
|
+
error: null,
|
|
838
|
+
logs: []
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
invoke: React.useCallback(async (body, opts) => {
|
|
842
|
+
if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
|
|
843
|
+
const { orgSlug, appSlug, apiBaseUrl } = resolved;
|
|
844
|
+
const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
|
|
845
|
+
setState((s) => ({
|
|
846
|
+
...s,
|
|
847
|
+
isLoading: true,
|
|
848
|
+
error: null
|
|
849
|
+
}));
|
|
850
|
+
try {
|
|
851
|
+
const headers = {
|
|
852
|
+
"content-type": "application/json",
|
|
853
|
+
accept: "text/event-stream"
|
|
854
|
+
};
|
|
855
|
+
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
856
|
+
const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
|
|
857
|
+
const resp = await fetcher(url, {
|
|
858
|
+
method: "POST",
|
|
859
|
+
headers,
|
|
860
|
+
body: JSON.stringify(body ?? {})
|
|
861
|
+
});
|
|
862
|
+
if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
|
|
863
|
+
return readFunctionSseStream(resp);
|
|
864
|
+
});
|
|
865
|
+
setState({
|
|
866
|
+
data: result.value,
|
|
867
|
+
isLoading: false,
|
|
868
|
+
error: null,
|
|
869
|
+
logs: result.logs
|
|
870
|
+
});
|
|
871
|
+
return result.value;
|
|
872
|
+
} catch (err) {
|
|
873
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
874
|
+
const logs = e.logs ?? [];
|
|
875
|
+
setState((s) => ({
|
|
876
|
+
...s,
|
|
877
|
+
isLoading: false,
|
|
878
|
+
error: e,
|
|
879
|
+
logs
|
|
880
|
+
}));
|
|
881
|
+
throw e;
|
|
882
|
+
}
|
|
883
|
+
}, [
|
|
884
|
+
resolved,
|
|
885
|
+
fetcher,
|
|
886
|
+
name
|
|
887
|
+
]),
|
|
888
|
+
data: state.data,
|
|
889
|
+
isLoading: state.isLoading,
|
|
890
|
+
error: state.error,
|
|
891
|
+
logs: state.logs
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
566
895
|
* Run a semantic-layer query against the project's `.view.yml` /
|
|
567
896
|
* `.topic.yml` definitions. The server compiles to SQL and executes
|
|
568
897
|
* through the same connector path as `useQuery`, so result shape
|
|
@@ -1008,7 +1337,7 @@ function parseSseData(raw) {
|
|
|
1008
1337
|
* uniformly. New types added upstream don't surface as artifacts
|
|
1009
1338
|
* until added here — that's intentional, the renderer needs to
|
|
1010
1339
|
* know how to display each. */
|
|
1011
|
-
const SQL_EVENT_TYPES = new Set([
|
|
1340
|
+
const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1012
1341
|
"query_executed",
|
|
1013
1342
|
"query_generated",
|
|
1014
1343
|
"verified_sql",
|
|
@@ -1137,6 +1466,82 @@ function sleep(ms, signal) {
|
|
|
1137
1466
|
});
|
|
1138
1467
|
}
|
|
1139
1468
|
/**
|
|
1469
|
+
* Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
|
|
1470
|
+
* `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
|
|
1471
|
+
* (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
|
|
1472
|
+
* grouped by name, with drill-down into recent occurrences.
|
|
1473
|
+
*
|
|
1474
|
+
* The handler returned by [`useTrackEvent`] is **fire-and-forget**:
|
|
1475
|
+
* it enqueues the event into an in-memory batch flushed every second
|
|
1476
|
+
* (and on `pagehide` so a navigation away doesn't drop the tail).
|
|
1477
|
+
* No await semantics — call it inline from a click handler without
|
|
1478
|
+
* awaiting it. Server-side validation errors are logged to the
|
|
1479
|
+
* console; the call site doesn't need to handle them.
|
|
1480
|
+
*
|
|
1481
|
+
* Example:
|
|
1482
|
+
* ```tsx
|
|
1483
|
+
* const track = useTrackEvent();
|
|
1484
|
+
* <button
|
|
1485
|
+
* onClick={() => {
|
|
1486
|
+
* track("export-clicked", { format: "csv", rowCount });
|
|
1487
|
+
* doExport();
|
|
1488
|
+
* }}
|
|
1489
|
+
* >Export</button>
|
|
1490
|
+
* ```
|
|
1491
|
+
*
|
|
1492
|
+
* Rate-limited at 60/min per (user, app) on the server. A burst that
|
|
1493
|
+
* trips the limit drops the excess events with a console warning;
|
|
1494
|
+
* within-limit events are unaffected.
|
|
1495
|
+
*/
|
|
1496
|
+
function useTrackEvent() {
|
|
1497
|
+
const { projectId, fetcher } = useOxyApp();
|
|
1498
|
+
const queueRef = React.useRef([]);
|
|
1499
|
+
const flushTimerRef = React.useRef(null);
|
|
1500
|
+
const flush = React.useCallback(() => {
|
|
1501
|
+
flushTimerRef.current = null;
|
|
1502
|
+
if (!projectId) return;
|
|
1503
|
+
const batch = queueRef.current;
|
|
1504
|
+
if (batch.length === 0) return;
|
|
1505
|
+
queueRef.current = [];
|
|
1506
|
+
for (const evt of batch) {
|
|
1507
|
+
const url = `/api/customer-apps/${projectId}/events`;
|
|
1508
|
+
const body = JSON.stringify(evt);
|
|
1509
|
+
try {
|
|
1510
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
1511
|
+
else fetcher(url, {
|
|
1512
|
+
method: "POST",
|
|
1513
|
+
headers: { "content-type": "application/json" },
|
|
1514
|
+
body
|
|
1515
|
+
}).catch((e) => {
|
|
1516
|
+
console.warn("[oxy] useTrackEvent flush failed:", e);
|
|
1517
|
+
});
|
|
1518
|
+
} catch (e) {
|
|
1519
|
+
console.warn("[oxy] useTrackEvent enqueue failed:", e);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}, [projectId, fetcher]);
|
|
1523
|
+
React.useEffect(() => {
|
|
1524
|
+
if (typeof window === "undefined") return;
|
|
1525
|
+
const onHide = () => flush();
|
|
1526
|
+
window.addEventListener("pagehide", onHide);
|
|
1527
|
+
return () => {
|
|
1528
|
+
window.removeEventListener("pagehide", onHide);
|
|
1529
|
+
flush();
|
|
1530
|
+
if (flushTimerRef.current !== null) {
|
|
1531
|
+
clearTimeout(flushTimerRef.current);
|
|
1532
|
+
flushTimerRef.current = null;
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
}, [flush]);
|
|
1536
|
+
return React.useCallback((name, payload) => {
|
|
1537
|
+
queueRef.current.push({
|
|
1538
|
+
event_name: name,
|
|
1539
|
+
payload: payload ?? {}
|
|
1540
|
+
});
|
|
1541
|
+
if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
|
|
1542
|
+
}, [flush]);
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1140
1545
|
* Renders an agent run's answer + artifacts + thread link as a
|
|
1141
1546
|
* single block. The default styling is intentionally neutral
|
|
1142
1547
|
* (system fonts, gray surfaces) so it blends into any bundle.
|
|
@@ -1158,25 +1563,55 @@ function OxyAnswer(props) {
|
|
|
1158
1563
|
const isRunning = state === "running";
|
|
1159
1564
|
const isFailed = state === "failed";
|
|
1160
1565
|
const needsClarification = state === "needs_clarification";
|
|
1161
|
-
return /* @__PURE__ */
|
|
1566
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1162
1567
|
className,
|
|
1163
|
-
style: styles.answerWrap
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1568
|
+
style: styles.answerWrap,
|
|
1569
|
+
children: [
|
|
1570
|
+
isRunning && answer === null ? /* @__PURE__ */ jsxs("div", {
|
|
1571
|
+
style: styles.statusRow,
|
|
1572
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1573
|
+
style: styles.spinner,
|
|
1574
|
+
"aria-hidden": "true"
|
|
1575
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1576
|
+
style: styles.statusText,
|
|
1577
|
+
children: "Thinking…"
|
|
1578
|
+
})]
|
|
1579
|
+
}) : null,
|
|
1580
|
+
artifacts.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
1581
|
+
style: styles.artifactList,
|
|
1582
|
+
children: artifacts.map((a) => /* @__PURE__ */ jsx(SqlArtifactBlock, {
|
|
1583
|
+
artifact: a,
|
|
1584
|
+
maxRows: maxArtifactRows
|
|
1585
|
+
}, a.id))
|
|
1586
|
+
}) : null,
|
|
1587
|
+
answer ? /* @__PURE__ */ jsx("div", {
|
|
1588
|
+
style: styles.markdown,
|
|
1589
|
+
children: /* @__PURE__ */ jsx(MarkdownText, { text: answer })
|
|
1590
|
+
}) : null,
|
|
1591
|
+
needsClarification && clarification ? /* @__PURE__ */ jsxs("div", {
|
|
1592
|
+
style: styles.clarification,
|
|
1593
|
+
children: [/* @__PURE__ */ jsx("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ jsx("div", {
|
|
1594
|
+
style: { marginTop: 4 },
|
|
1595
|
+
children: clarification
|
|
1596
|
+
})]
|
|
1597
|
+
}) : null,
|
|
1598
|
+
isFailed && error ? /* @__PURE__ */ jsx(ErrorBlock, { error }) : null,
|
|
1599
|
+
threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ jsxs("div", {
|
|
1600
|
+
style: styles.threadLinkRow,
|
|
1601
|
+
children: [/* @__PURE__ */ jsxs("a", {
|
|
1602
|
+
href: threadUrl,
|
|
1603
|
+
target: "_blank",
|
|
1604
|
+
rel: "noreferrer noopener",
|
|
1605
|
+
style: styles.threadLink,
|
|
1606
|
+
children: [threadLinkLabel, " →"]
|
|
1607
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1608
|
+
style: styles.betaBadge,
|
|
1609
|
+
title: "Thread linking is in beta — see docs",
|
|
1610
|
+
children: "beta"
|
|
1611
|
+
})]
|
|
1612
|
+
}) : null
|
|
1613
|
+
]
|
|
1614
|
+
});
|
|
1180
1615
|
}
|
|
1181
1616
|
/**
|
|
1182
1617
|
* Complete drop-in chat surface. One agent, one input, one answer
|
|
@@ -1200,37 +1635,48 @@ function OxyChat(props) {
|
|
|
1200
1635
|
if (!q || run.state === "running") return;
|
|
1201
1636
|
run.ask(q);
|
|
1202
1637
|
}, [question, run]);
|
|
1203
|
-
return /* @__PURE__ */
|
|
1638
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1204
1639
|
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
|
-
|
|
1640
|
+
style: styles.chatWrap,
|
|
1641
|
+
children: [/* @__PURE__ */ jsxs("form", {
|
|
1642
|
+
onSubmit: submit,
|
|
1643
|
+
style: styles.chatForm,
|
|
1644
|
+
children: [
|
|
1645
|
+
/* @__PURE__ */ jsx("input", {
|
|
1646
|
+
type: "text",
|
|
1647
|
+
value: question,
|
|
1648
|
+
onChange: (e) => setQuestion(e.target.value),
|
|
1649
|
+
placeholder,
|
|
1650
|
+
disabled: run.state === "running",
|
|
1651
|
+
style: styles.chatInput,
|
|
1652
|
+
"aria-label": "Question"
|
|
1653
|
+
}),
|
|
1654
|
+
/* @__PURE__ */ jsx("button", {
|
|
1655
|
+
type: "submit",
|
|
1656
|
+
disabled: run.state === "running" || question.trim() === "",
|
|
1657
|
+
style: styles.chatSubmit,
|
|
1658
|
+
children: run.state === "running" ? "…" : submitLabel
|
|
1659
|
+
}),
|
|
1660
|
+
run.state === "running" ? /* @__PURE__ */ jsx("button", {
|
|
1661
|
+
type: "button",
|
|
1662
|
+
onClick: run.cancel,
|
|
1663
|
+
style: styles.chatCancel,
|
|
1664
|
+
children: "Stop"
|
|
1665
|
+
}) : null
|
|
1666
|
+
]
|
|
1667
|
+
}), run.state === "idle" ? emptyState ?? /* @__PURE__ */ jsx("div", {
|
|
1668
|
+
style: styles.emptyState,
|
|
1669
|
+
children: "Ask a question to get started."
|
|
1670
|
+
}) : /* @__PURE__ */ jsx(OxyAnswer, {
|
|
1671
|
+
answer: run.answer,
|
|
1672
|
+
artifacts: run.artifacts,
|
|
1673
|
+
state: run.state,
|
|
1674
|
+
clarification: run.clarification,
|
|
1675
|
+
error: run.error,
|
|
1676
|
+
threadUrl: run.threadUrl,
|
|
1677
|
+
maxArtifactRows
|
|
1678
|
+
})]
|
|
1679
|
+
});
|
|
1234
1680
|
}
|
|
1235
1681
|
function SqlArtifactBlock(props) {
|
|
1236
1682
|
const { artifact, maxRows } = props;
|
|
@@ -1239,17 +1685,53 @@ function SqlArtifactBlock(props) {
|
|
|
1239
1685
|
const truncated = results ? results.rows.length > maxRows : false;
|
|
1240
1686
|
const visibleRows = results ? results.rows.slice(0, maxRows) : [];
|
|
1241
1687
|
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
|
-
|
|
1688
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1689
|
+
style: styles.artifact,
|
|
1690
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
1691
|
+
type: "button",
|
|
1692
|
+
onClick: () => setOpen((o) => !o),
|
|
1693
|
+
style: styles.artifactHeader,
|
|
1694
|
+
children: [
|
|
1695
|
+
/* @__PURE__ */ jsx("span", {
|
|
1696
|
+
style: styles.artifactBadge,
|
|
1697
|
+
children: sourceLabel
|
|
1698
|
+
}),
|
|
1699
|
+
/* @__PURE__ */ jsx("span", {
|
|
1700
|
+
style: styles.artifactSummary,
|
|
1701
|
+
children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
|
|
1702
|
+
}),
|
|
1703
|
+
/* @__PURE__ */ jsx("span", {
|
|
1704
|
+
style: styles.artifactToggle,
|
|
1705
|
+
children: open ? "Hide" : "Show"
|
|
1706
|
+
})
|
|
1707
|
+
]
|
|
1708
|
+
}), open ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("pre", {
|
|
1709
|
+
style: styles.sqlBlock,
|
|
1710
|
+
children: artifact.sql
|
|
1711
|
+
}), artifact.error ? /* @__PURE__ */ jsx("div", {
|
|
1712
|
+
style: styles.error,
|
|
1713
|
+
children: artifact.error
|
|
1714
|
+
}) : results ? /* @__PURE__ */ jsxs("div", {
|
|
1715
|
+
style: styles.resultsWrap,
|
|
1716
|
+
children: [/* @__PURE__ */ jsxs("table", {
|
|
1717
|
+
style: styles.resultsTable,
|
|
1718
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: results.columns.map((c) => /* @__PURE__ */ jsx("th", {
|
|
1719
|
+
style: styles.resultsTh,
|
|
1720
|
+
children: c
|
|
1721
|
+
}, c)) }) }), /* @__PURE__ */ jsx("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx("td", {
|
|
1722
|
+
style: styles.resultsTd,
|
|
1723
|
+
children: formatCell(cell)
|
|
1724
|
+
}, j)) }, i)) })]
|
|
1725
|
+
}), truncated ? /* @__PURE__ */ jsxs("div", {
|
|
1726
|
+
style: styles.truncatedNote,
|
|
1727
|
+
children: [
|
|
1728
|
+
"+",
|
|
1729
|
+
results.rows.length - maxRows,
|
|
1730
|
+
" more rows. Open the thread in Oxy to see all."
|
|
1731
|
+
]
|
|
1732
|
+
}) : null]
|
|
1733
|
+
}) : null] }) : null]
|
|
1734
|
+
});
|
|
1253
1735
|
}
|
|
1254
1736
|
function formatCell(value) {
|
|
1255
1737
|
if (value === null || value === void 0) return "—";
|
|
@@ -1264,16 +1746,36 @@ function formatCell(value) {
|
|
|
1264
1746
|
*/
|
|
1265
1747
|
function ErrorBlock(props) {
|
|
1266
1748
|
const { error } = props;
|
|
1267
|
-
if (error instanceof OxyApiError) return /* @__PURE__ */
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1749
|
+
if (error instanceof OxyApiError) return /* @__PURE__ */ jsxs("div", {
|
|
1750
|
+
style: styles.error,
|
|
1751
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
1752
|
+
/* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
|
|
1753
|
+
" ",
|
|
1754
|
+
error.message.split("\n\n")[0]
|
|
1755
|
+
] }), error.hint ? /* @__PURE__ */ jsxs("div", {
|
|
1756
|
+
style: {
|
|
1757
|
+
marginTop: 6,
|
|
1758
|
+
fontWeight: 400,
|
|
1759
|
+
whiteSpace: "pre-wrap"
|
|
1760
|
+
},
|
|
1761
|
+
children: [
|
|
1762
|
+
/* @__PURE__ */ jsx("strong", { children: "Hint:" }),
|
|
1763
|
+
" ",
|
|
1764
|
+
error.hint
|
|
1765
|
+
]
|
|
1766
|
+
}) : null]
|
|
1767
|
+
});
|
|
1768
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1769
|
+
style: styles.error,
|
|
1770
|
+
children: [
|
|
1771
|
+
/* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
|
|
1772
|
+
" ",
|
|
1773
|
+
error.message
|
|
1774
|
+
]
|
|
1775
|
+
});
|
|
1273
1776
|
}
|
|
1274
1777
|
function MarkdownText(props) {
|
|
1275
|
-
|
|
1276
|
-
return /* @__PURE__ */ React.createElement(React.Fragment, null, blocks);
|
|
1778
|
+
return /* @__PURE__ */ jsx(Fragment, { children: React.useMemo(() => parseMarkdown(props.text), [props.text]) });
|
|
1277
1779
|
}
|
|
1278
1780
|
function parseMarkdown(text) {
|
|
1279
1781
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
@@ -1343,27 +1845,23 @@ function parseMarkdown(text) {
|
|
|
1343
1845
|
}
|
|
1344
1846
|
return blocks.map((b, idx) => {
|
|
1345
1847
|
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,
|
|
1848
|
+
case "h": return /* @__PURE__ */ jsx(`h${b.level}`, {
|
|
1849
|
+
style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
|
|
1850
|
+
children: renderInline(b.text)
|
|
1851
|
+
}, idx);
|
|
1852
|
+
case "code": return /* @__PURE__ */ jsx("pre", {
|
|
1356
1853
|
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
|
-
|
|
1854
|
+
"data-lang": b.lang || void 0,
|
|
1855
|
+
children: /* @__PURE__ */ jsx("code", { children: b.code })
|
|
1856
|
+
}, idx);
|
|
1857
|
+
case "list": return /* @__PURE__ */ jsx("ul", {
|
|
1858
|
+
style: styles.list,
|
|
1859
|
+
children: b.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: renderInline(item) }, i))
|
|
1860
|
+
}, idx);
|
|
1861
|
+
case "p": return /* @__PURE__ */ jsx("p", {
|
|
1862
|
+
style: styles.paragraph,
|
|
1863
|
+
children: renderInline(b.text)
|
|
1864
|
+
}, idx);
|
|
1367
1865
|
}
|
|
1368
1866
|
});
|
|
1369
1867
|
}
|
|
@@ -1379,27 +1877,31 @@ function renderInline(text) {
|
|
|
1379
1877
|
const patterns = [
|
|
1380
1878
|
{
|
|
1381
1879
|
re: /`([^`]+)`/,
|
|
1382
|
-
render: (m) => /* @__PURE__ */
|
|
1880
|
+
render: (m) => /* @__PURE__ */ jsx("code", {
|
|
1881
|
+
style: styles.inlineCode,
|
|
1882
|
+
children: m[1]
|
|
1883
|
+
})
|
|
1383
1884
|
},
|
|
1384
1885
|
{
|
|
1385
1886
|
re: /\[([^\]]+)\]\(([^)]+)\)/,
|
|
1386
1887
|
render: (m) => {
|
|
1387
|
-
if (isSafeLinkHref(m[2])) return /* @__PURE__ */
|
|
1888
|
+
if (isSafeLinkHref(m[2])) return /* @__PURE__ */ jsx("a", {
|
|
1388
1889
|
href: m[2],
|
|
1389
1890
|
target: "_blank",
|
|
1390
1891
|
rel: "noreferrer noopener",
|
|
1391
|
-
style: styles.link
|
|
1392
|
-
|
|
1393
|
-
|
|
1892
|
+
style: styles.link,
|
|
1893
|
+
children: m[1]
|
|
1894
|
+
});
|
|
1895
|
+
return /* @__PURE__ */ jsx(Fragment, { children: m[1] });
|
|
1394
1896
|
}
|
|
1395
1897
|
},
|
|
1396
1898
|
{
|
|
1397
1899
|
re: /\*\*([^*]+)\*\*/,
|
|
1398
|
-
render: (m) => /* @__PURE__ */
|
|
1900
|
+
render: (m) => /* @__PURE__ */ jsx("strong", { children: m[1] })
|
|
1399
1901
|
},
|
|
1400
1902
|
{
|
|
1401
1903
|
re: /\*([^*]+)\*/,
|
|
1402
|
-
render: (m) => /* @__PURE__ */
|
|
1904
|
+
render: (m) => /* @__PURE__ */ jsx("em", { children: m[1] })
|
|
1403
1905
|
}
|
|
1404
1906
|
];
|
|
1405
1907
|
while (remaining.length > 0) {
|
|
@@ -1417,7 +1919,7 @@ function renderInline(text) {
|
|
|
1417
1919
|
break;
|
|
1418
1920
|
}
|
|
1419
1921
|
if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
|
|
1420
|
-
segments.push(/* @__PURE__ */
|
|
1922
|
+
segments.push(/* @__PURE__ */ jsx(React.Fragment, { children: earliest.node }, key++));
|
|
1421
1923
|
remaining = remaining.slice(earliest.idx + earliest.len);
|
|
1422
1924
|
}
|
|
1423
1925
|
return segments;
|
|
@@ -1647,5 +2149,133 @@ const styles = {
|
|
|
1647
2149
|
};
|
|
1648
2150
|
|
|
1649
2151
|
//#endregion
|
|
1650
|
-
|
|
2152
|
+
//#region src/metricTree.ts
|
|
2153
|
+
/**
|
|
2154
|
+
* Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
|
|
2155
|
+
* airlayer metric-tree analyses (tree introspection, sensitivity, predict,
|
|
2156
|
+
* explain, opportunity) over typed methods.
|
|
2157
|
+
*
|
|
2158
|
+
* Construction is internal to {@link OxyClient} — call `client.metricTree`
|
|
2159
|
+
* to access an instance rather than building one yourself.
|
|
2160
|
+
*
|
|
2161
|
+
* @example
|
|
2162
|
+
* ```typescript
|
|
2163
|
+
* const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
|
|
2164
|
+
* const tree = await client.metricTree.getTree();
|
|
2165
|
+
* const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2166
|
+
* ```
|
|
2167
|
+
*/
|
|
2168
|
+
var MetricTreeClient = class {
|
|
2169
|
+
constructor(config, request) {
|
|
2170
|
+
this.config = config;
|
|
2171
|
+
this.request = request;
|
|
2172
|
+
}
|
|
2173
|
+
path(suffix) {
|
|
2174
|
+
return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
|
|
2175
|
+
}
|
|
2176
|
+
buildQuery(extra = {}) {
|
|
2177
|
+
const params = { ...extra };
|
|
2178
|
+
if (this.config.branch) params.branch = this.config.branch;
|
|
2179
|
+
const qs = new URLSearchParams(params).toString();
|
|
2180
|
+
return qs ? `?${qs}` : "";
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Fetch the full metric tree, or the subtree rooted at `root`.
|
|
2184
|
+
*
|
|
2185
|
+
* @param root - Optional fully-qualified measure id to root the tree at.
|
|
2186
|
+
* @returns Nodes (measures) and edges (component / driver relationships).
|
|
2187
|
+
*
|
|
2188
|
+
* @example
|
|
2189
|
+
* ```typescript
|
|
2190
|
+
* const tree = await client.metricTree.getTree();
|
|
2191
|
+
* const subtree = await client.metricTree.getTree("orders.net_revenue");
|
|
2192
|
+
* ```
|
|
2193
|
+
*/
|
|
2194
|
+
async getTree(root) {
|
|
2195
|
+
const query = this.buildQuery(root ? { root } : {});
|
|
2196
|
+
return this.request(this.path(query));
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Rank the declared drivers of a measure by influence.
|
|
2200
|
+
*
|
|
2201
|
+
* @param measureId - Fully-qualified measure id (`view.measure`).
|
|
2202
|
+
*
|
|
2203
|
+
* @example
|
|
2204
|
+
* ```typescript
|
|
2205
|
+
* const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2206
|
+
* for (const driver of sensitivity.drivers) {
|
|
2207
|
+
* console.log(driver.measure, driver.direction, driver.strength);
|
|
2208
|
+
* }
|
|
2209
|
+
* ```
|
|
2210
|
+
*/
|
|
2211
|
+
async getSensitivity(measureId) {
|
|
2212
|
+
const query = this.buildQuery();
|
|
2213
|
+
return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Propagate hypothetical `(measure, delta)` changes upward through the
|
|
2217
|
+
* tree. Returns the estimated impact on every downstream measure.
|
|
2218
|
+
*
|
|
2219
|
+
* @example
|
|
2220
|
+
* ```typescript
|
|
2221
|
+
* const result = await client.metricTree.predict([
|
|
2222
|
+
* { measure: "marketing_spend.total_spend", delta: 10000 },
|
|
2223
|
+
* ]);
|
|
2224
|
+
* ```
|
|
2225
|
+
*/
|
|
2226
|
+
async predict(changes) {
|
|
2227
|
+
const query = this.buildQuery();
|
|
2228
|
+
return this.request(this.path(`/predict${query}`), {
|
|
2229
|
+
method: "POST",
|
|
2230
|
+
body: JSON.stringify({ changes })
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Period-over-period root-cause decomposition. Recursively splits the
|
|
2235
|
+
* target measure by components and dimensions until the move concentrates.
|
|
2236
|
+
*
|
|
2237
|
+
* @example
|
|
2238
|
+
* ```typescript
|
|
2239
|
+
* const result = await client.metricTree.explain({
|
|
2240
|
+
* target: "financials.operating_profit",
|
|
2241
|
+
* time_dimension: "financials.month",
|
|
2242
|
+
* current_period: ["2025-09-01", "2025-09-30"],
|
|
2243
|
+
* previous_period: ["2025-08-01", "2025-08-31"],
|
|
2244
|
+
* });
|
|
2245
|
+
* ```
|
|
2246
|
+
*/
|
|
2247
|
+
async explain(request) {
|
|
2248
|
+
const query = this.buildQuery();
|
|
2249
|
+
return this.request(this.path(`/explain${query}`), {
|
|
2250
|
+
method: "POST",
|
|
2251
|
+
body: JSON.stringify(request)
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Size the upside opportunity for a measure by finding underperforming
|
|
2256
|
+
* segments. Skips high-cardinality dimensions and trims the long tail.
|
|
2257
|
+
*
|
|
2258
|
+
* @example
|
|
2259
|
+
* ```typescript
|
|
2260
|
+
* const result = await client.metricTree.findOpportunities({
|
|
2261
|
+
* target: "orders.net_revenue",
|
|
2262
|
+
* time_dimension: "orders.order_date",
|
|
2263
|
+
* period: ["2025-09-01", "2025-09-30"],
|
|
2264
|
+
* });
|
|
2265
|
+
* for (const dim of result.dimensions) {
|
|
2266
|
+
* console.log(dim.dimension, "+", dim.total_upside);
|
|
2267
|
+
* }
|
|
2268
|
+
* ```
|
|
2269
|
+
*/
|
|
2270
|
+
async findOpportunities(request) {
|
|
2271
|
+
const query = this.buildQuery();
|
|
2272
|
+
return this.request(this.path(`/opportunity${query}`), {
|
|
2273
|
+
method: "POST",
|
|
2274
|
+
body: JSON.stringify(request)
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
//#endregion
|
|
2280
|
+
export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
|
|
1651
2281
|
//# sourceMappingURL=index.mjs.map
|