@oxy-hq/sdk 2.0.0 → 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 +20 -15
package/dist/index.cjs
CHANGED
|
@@ -28,8 +28,90 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
|
|
29
29
|
//#endregion
|
|
30
30
|
let react = require("react");
|
|
31
|
-
react = __toESM(react);
|
|
31
|
+
react = __toESM(react, 1);
|
|
32
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
32
33
|
|
|
34
|
+
//#region src/anomalies.ts
|
|
35
|
+
/**
|
|
36
|
+
* Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`
|
|
37
|
+
* rather than instantiating directly — the getter wires the request helper
|
|
38
|
+
* so auth, timeout, and branch propagation come along for free.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const { anomalies } = await client.anomalies.list({ status: "new" });
|
|
43
|
+
* for (const a of anomalies) {
|
|
44
|
+
* console.log(a.label ?? a.measure, a.severity, a.z_score.toFixed(2));
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
var AnomaliesClient = class {
|
|
49
|
+
constructor(config, request) {
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.request = request;
|
|
52
|
+
}
|
|
53
|
+
path(suffix) {
|
|
54
|
+
return `/${this.config.projectId}/semantic/anomalies${suffix}`;
|
|
55
|
+
}
|
|
56
|
+
buildQuery(extra = {}) {
|
|
57
|
+
const params = { ...extra };
|
|
58
|
+
if (this.config.branch) params.branch = this.config.branch;
|
|
59
|
+
const qs = new URLSearchParams(params).toString();
|
|
60
|
+
return qs ? `?${qs}` : "";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* List anomalies in the inbox, newest first.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* // Open / unresolved anomalies only
|
|
68
|
+
* const { anomalies } = await client.anomalies.list({ status: "new" });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
async list(options = {}) {
|
|
72
|
+
const extra = {};
|
|
73
|
+
if (options.status) extra.status = options.status;
|
|
74
|
+
if (options.limit) extra.limit = String(options.limit);
|
|
75
|
+
return this.request(this.path(this.buildQuery(extra)));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Trigger a full scan. Iterates every `.monitor.yml` entry in the
|
|
79
|
+
* workspace, runs the detector, and upserts matching rows into the
|
|
80
|
+
* inbox. Returns counts of scanned / failed / persisted.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* // Scan against a known-good reference date (matches the seed dataset)
|
|
85
|
+
* const result = await client.anomalies.scan({ as_of: "2025-12-15" });
|
|
86
|
+
* console.log(`${result.anomalies_persisted} anomalies detected`);
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
async scan(options = {}) {
|
|
90
|
+
const extra = {};
|
|
91
|
+
if (options.as_of) extra.as_of = options.as_of;
|
|
92
|
+
return this.request(this.path(`/scan${this.buildQuery(extra)}`), { method: "POST" });
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Update an anomaly's status (acknowledge / dismiss / re-open).
|
|
96
|
+
*/
|
|
97
|
+
async updateStatus(anomalyId, status) {
|
|
98
|
+
const query = this.buildQuery();
|
|
99
|
+
return this.request(this.path(`/${encodeURIComponent(anomalyId)}/status${query}`), {
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: JSON.stringify({ status })
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run the metric-tree `explain` for an anomaly and cache the result on
|
|
106
|
+
* the row. Subsequent calls return the cached `ExplainResult` instantly.
|
|
107
|
+
*/
|
|
108
|
+
async explain(anomalyId) {
|
|
109
|
+
const query = this.buildQuery();
|
|
110
|
+
return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`), { method: "POST" });
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
33
115
|
//#region src/customer-app/logger.ts
|
|
34
116
|
let activeLogger = createConsoleLogger();
|
|
35
117
|
/** Replace the global logger. Pass `null` to silence everything. */
|
|
@@ -284,13 +366,156 @@ function validateManifest(raw, url) {
|
|
|
284
366
|
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
285
367
|
slug: raw.slug,
|
|
286
368
|
orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
|
|
287
|
-
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
|
|
369
|
+
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
|
|
370
|
+
functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
|
|
288
371
|
};
|
|
289
372
|
}
|
|
373
|
+
const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
374
|
+
/**
|
|
375
|
+
* Validate the optional `functions` map. Each key is a function name;
|
|
376
|
+
* each value declares how the function is invoked. Mirrors the
|
|
377
|
+
* server-side validation in `customer_apps_publish.rs` so a bad
|
|
378
|
+
* manifest fails at build, not at publish.
|
|
379
|
+
*/
|
|
380
|
+
function validateFunctions(raw) {
|
|
381
|
+
if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
|
|
382
|
+
const out = {};
|
|
383
|
+
for (const [fnName, value] of Object.entries(raw)) {
|
|
384
|
+
if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
|
|
385
|
+
if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
|
|
386
|
+
const fn = {};
|
|
387
|
+
if (value.entry !== void 0) {
|
|
388
|
+
if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
|
|
389
|
+
fn.entry = value.entry;
|
|
390
|
+
}
|
|
391
|
+
if (value.schedule !== void 0) {
|
|
392
|
+
if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
|
|
393
|
+
fn.schedule = value.schedule;
|
|
394
|
+
}
|
|
395
|
+
if (value.timezone !== void 0) {
|
|
396
|
+
if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
|
|
397
|
+
fn.timezone = value.timezone;
|
|
398
|
+
}
|
|
399
|
+
if (value.route !== void 0) {
|
|
400
|
+
if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
|
|
401
|
+
fn.route = value.route;
|
|
402
|
+
}
|
|
403
|
+
if (value.airwayStep !== void 0) {
|
|
404
|
+
const step = value.airwayStep;
|
|
405
|
+
if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
|
|
406
|
+
fn.airwayStep = {
|
|
407
|
+
pipeline: step.pipeline,
|
|
408
|
+
resource: step.resource
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (value.timeoutSeconds !== void 0) {
|
|
412
|
+
const t = value.timeoutSeconds;
|
|
413
|
+
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]`);
|
|
414
|
+
fn.timeoutSeconds = t;
|
|
415
|
+
}
|
|
416
|
+
if (value.cache !== void 0) {
|
|
417
|
+
const c = value.cache;
|
|
418
|
+
if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
|
|
419
|
+
if (c.ttlSeconds !== void 0) {
|
|
420
|
+
const ttl = c.ttlSeconds;
|
|
421
|
+
if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
|
|
422
|
+
fn.cache = { ttlSeconds: ttl };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const hasSchedule = fn.schedule !== void 0;
|
|
426
|
+
const hasAirway = fn.airwayStep !== void 0;
|
|
427
|
+
if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
|
|
428
|
+
out[fnName] = fn;
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
290
432
|
function isRecord(v) {
|
|
291
433
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
292
434
|
}
|
|
293
435
|
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/customer-app/function-invoke.ts
|
|
438
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
439
|
+
/**
|
|
440
|
+
* Dedup key for an invocation: function name + its (stable-serialized) body,
|
|
441
|
+
* joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
|
|
442
|
+
* the separator can never collide with a name.
|
|
443
|
+
*/
|
|
444
|
+
function functionInvokeKey(name, body) {
|
|
445
|
+
return `${name}\n${JSON.stringify(body ?? {})}`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Run `run()` unless an identical invocation is already in flight, in which
|
|
449
|
+
* case share its promise. The entry is removed once the promise settles — so
|
|
450
|
+
* this dedups concurrency only, it does NOT memoize the result.
|
|
451
|
+
*/
|
|
452
|
+
function sharedFunctionInvoke(key, run) {
|
|
453
|
+
const existing = inflight.get(key);
|
|
454
|
+
if (existing) return existing;
|
|
455
|
+
const p = run().finally(() => {
|
|
456
|
+
inflight.delete(key);
|
|
457
|
+
});
|
|
458
|
+
inflight.set(key, p);
|
|
459
|
+
return p;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/customer-app/function-sse.ts
|
|
464
|
+
/**
|
|
465
|
+
* Read a `text/event-stream` function response to completion. Resolves with the
|
|
466
|
+
* decoded result + captured logs, or rejects (with `.logs` attached) on an
|
|
467
|
+
* `event: error` frame / a stream that ends without a terminal event.
|
|
468
|
+
*/
|
|
469
|
+
async function readFunctionSseStream(resp) {
|
|
470
|
+
const reader = resp.body?.getReader();
|
|
471
|
+
if (!reader) throw new Error("function response has no body stream");
|
|
472
|
+
const decoder = new TextDecoder();
|
|
473
|
+
let buffer = "";
|
|
474
|
+
let dataPayload = "";
|
|
475
|
+
const logs = [];
|
|
476
|
+
const handleFrame = (frame) => {
|
|
477
|
+
let event = "message";
|
|
478
|
+
let data = "";
|
|
479
|
+
for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
480
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
481
|
+
if (event === "log") try {
|
|
482
|
+
const l = JSON.parse(data);
|
|
483
|
+
logs.push({
|
|
484
|
+
level: String(l.level ?? "info"),
|
|
485
|
+
message: String(l.message ?? "")
|
|
486
|
+
});
|
|
487
|
+
} catch {}
|
|
488
|
+
else if (event === "data") dataPayload = data;
|
|
489
|
+
else if (event === "done") return {
|
|
490
|
+
done: true,
|
|
491
|
+
value: dataPayload ? JSON.parse(dataPayload) : null
|
|
492
|
+
};
|
|
493
|
+
else if (event === "error") {
|
|
494
|
+
const payload = data ? JSON.parse(data) : {};
|
|
495
|
+
const err = new Error(payload.message || payload.error || "function invocation failed");
|
|
496
|
+
err.name = payload.error || "FunctionError";
|
|
497
|
+
err.logs = logs;
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
for (;;) {
|
|
502
|
+
const { done, value } = await reader.read();
|
|
503
|
+
if (done) break;
|
|
504
|
+
buffer += decoder.decode(value, { stream: true });
|
|
505
|
+
let sep;
|
|
506
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
507
|
+
const frame = buffer.slice(0, sep);
|
|
508
|
+
buffer = buffer.slice(sep + 2);
|
|
509
|
+
const result = handleFrame(frame);
|
|
510
|
+
if (result) return {
|
|
511
|
+
value: result.value,
|
|
512
|
+
logs
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
throw new Error("function stream ended without a terminal event");
|
|
517
|
+
}
|
|
518
|
+
|
|
294
519
|
//#endregion
|
|
295
520
|
//#region src/customer-app/interpolate.ts
|
|
296
521
|
/**
|
|
@@ -396,26 +621,55 @@ function OxyAppProvider(props) {
|
|
|
396
621
|
}, [manifestOptions, fetcher]);
|
|
397
622
|
if (state.status === "error" && state.error) {
|
|
398
623
|
const err = state.error;
|
|
399
|
-
return /* @__PURE__ */
|
|
624
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
|
|
625
|
+
value: state,
|
|
626
|
+
children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
|
|
627
|
+
});
|
|
400
628
|
}
|
|
401
|
-
if (state.status === "loading") return /* @__PURE__ */
|
|
402
|
-
|
|
629
|
+
if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
|
|
630
|
+
value: state,
|
|
631
|
+
children: fallback ?? null
|
|
632
|
+
});
|
|
633
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
|
|
634
|
+
value: state,
|
|
635
|
+
children
|
|
636
|
+
});
|
|
403
637
|
}
|
|
404
638
|
function defaultErrorFallback(err) {
|
|
405
|
-
return /* @__PURE__ */
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
639
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
640
|
+
style: {
|
|
641
|
+
margin: "2rem auto",
|
|
642
|
+
maxWidth: "640px",
|
|
643
|
+
padding: "1rem",
|
|
644
|
+
border: "1px solid #fca5a5",
|
|
645
|
+
background: "#fee2e2",
|
|
646
|
+
color: "#991b1b",
|
|
647
|
+
borderRadius: "8px",
|
|
648
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
649
|
+
fontSize: "14px"
|
|
650
|
+
},
|
|
651
|
+
children: [
|
|
652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
653
|
+
style: { fontWeight: 600 },
|
|
654
|
+
children: err.title
|
|
655
|
+
}),
|
|
656
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
657
|
+
style: {
|
|
658
|
+
fontSize: "12px",
|
|
659
|
+
marginTop: "4px"
|
|
660
|
+
},
|
|
661
|
+
children: err.message
|
|
662
|
+
}),
|
|
663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
664
|
+
style: { marginTop: "12px" },
|
|
665
|
+
children: [
|
|
666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "What to try:" }),
|
|
667
|
+
" ",
|
|
668
|
+
err.hint
|
|
669
|
+
]
|
|
670
|
+
})
|
|
671
|
+
]
|
|
672
|
+
});
|
|
419
673
|
}
|
|
420
674
|
/**
|
|
421
675
|
* Emit a one-time `console.warn` the first time a beta hook is
|
|
@@ -592,6 +846,81 @@ function useQuery(input, opts = {}) {
|
|
|
592
846
|
};
|
|
593
847
|
}
|
|
594
848
|
/**
|
|
849
|
+
* Imperative hook for invoking an Oxy Function by name.
|
|
850
|
+
*
|
|
851
|
+
* ```tsx
|
|
852
|
+
* const refresh = useFunction("refresh-sales");
|
|
853
|
+
* <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
|
|
854
|
+
* Refresh
|
|
855
|
+
* </button>
|
|
856
|
+
* ```
|
|
857
|
+
*/
|
|
858
|
+
function useFunction(name) {
|
|
859
|
+
const ctx = react.useContext(OxyAppContext);
|
|
860
|
+
if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
|
|
861
|
+
const fetcher = ctx.fetcher;
|
|
862
|
+
const resolved = ctx.resolved;
|
|
863
|
+
const [state, setState] = react.useState({
|
|
864
|
+
data: null,
|
|
865
|
+
isLoading: false,
|
|
866
|
+
error: null,
|
|
867
|
+
logs: []
|
|
868
|
+
});
|
|
869
|
+
return {
|
|
870
|
+
invoke: react.useCallback(async (body, opts) => {
|
|
871
|
+
if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
|
|
872
|
+
const { orgSlug, appSlug, apiBaseUrl } = resolved;
|
|
873
|
+
const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
|
|
874
|
+
setState((s) => ({
|
|
875
|
+
...s,
|
|
876
|
+
isLoading: true,
|
|
877
|
+
error: null
|
|
878
|
+
}));
|
|
879
|
+
try {
|
|
880
|
+
const headers = {
|
|
881
|
+
"content-type": "application/json",
|
|
882
|
+
accept: "text/event-stream"
|
|
883
|
+
};
|
|
884
|
+
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
885
|
+
const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
|
|
886
|
+
const resp = await fetcher(url, {
|
|
887
|
+
method: "POST",
|
|
888
|
+
headers,
|
|
889
|
+
body: JSON.stringify(body ?? {})
|
|
890
|
+
});
|
|
891
|
+
if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
|
|
892
|
+
return readFunctionSseStream(resp);
|
|
893
|
+
});
|
|
894
|
+
setState({
|
|
895
|
+
data: result.value,
|
|
896
|
+
isLoading: false,
|
|
897
|
+
error: null,
|
|
898
|
+
logs: result.logs
|
|
899
|
+
});
|
|
900
|
+
return result.value;
|
|
901
|
+
} catch (err) {
|
|
902
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
903
|
+
const logs = e.logs ?? [];
|
|
904
|
+
setState((s) => ({
|
|
905
|
+
...s,
|
|
906
|
+
isLoading: false,
|
|
907
|
+
error: e,
|
|
908
|
+
logs
|
|
909
|
+
}));
|
|
910
|
+
throw e;
|
|
911
|
+
}
|
|
912
|
+
}, [
|
|
913
|
+
resolved,
|
|
914
|
+
fetcher,
|
|
915
|
+
name
|
|
916
|
+
]),
|
|
917
|
+
data: state.data,
|
|
918
|
+
isLoading: state.isLoading,
|
|
919
|
+
error: state.error,
|
|
920
|
+
logs: state.logs
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
595
924
|
* Run a semantic-layer query against the project's `.view.yml` /
|
|
596
925
|
* `.topic.yml` definitions. The server compiles to SQL and executes
|
|
597
926
|
* through the same connector path as `useQuery`, so result shape
|
|
@@ -1037,7 +1366,7 @@ function parseSseData(raw) {
|
|
|
1037
1366
|
* uniformly. New types added upstream don't surface as artifacts
|
|
1038
1367
|
* until added here — that's intentional, the renderer needs to
|
|
1039
1368
|
* know how to display each. */
|
|
1040
|
-
const SQL_EVENT_TYPES = new Set([
|
|
1369
|
+
const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1041
1370
|
"query_executed",
|
|
1042
1371
|
"query_generated",
|
|
1043
1372
|
"verified_sql",
|
|
@@ -1166,6 +1495,82 @@ function sleep(ms, signal) {
|
|
|
1166
1495
|
});
|
|
1167
1496
|
}
|
|
1168
1497
|
/**
|
|
1498
|
+
* Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
|
|
1499
|
+
* `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
|
|
1500
|
+
* (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
|
|
1501
|
+
* grouped by name, with drill-down into recent occurrences.
|
|
1502
|
+
*
|
|
1503
|
+
* The handler returned by [`useTrackEvent`] is **fire-and-forget**:
|
|
1504
|
+
* it enqueues the event into an in-memory batch flushed every second
|
|
1505
|
+
* (and on `pagehide` so a navigation away doesn't drop the tail).
|
|
1506
|
+
* No await semantics — call it inline from a click handler without
|
|
1507
|
+
* awaiting it. Server-side validation errors are logged to the
|
|
1508
|
+
* console; the call site doesn't need to handle them.
|
|
1509
|
+
*
|
|
1510
|
+
* Example:
|
|
1511
|
+
* ```tsx
|
|
1512
|
+
* const track = useTrackEvent();
|
|
1513
|
+
* <button
|
|
1514
|
+
* onClick={() => {
|
|
1515
|
+
* track("export-clicked", { format: "csv", rowCount });
|
|
1516
|
+
* doExport();
|
|
1517
|
+
* }}
|
|
1518
|
+
* >Export</button>
|
|
1519
|
+
* ```
|
|
1520
|
+
*
|
|
1521
|
+
* Rate-limited at 60/min per (user, app) on the server. A burst that
|
|
1522
|
+
* trips the limit drops the excess events with a console warning;
|
|
1523
|
+
* within-limit events are unaffected.
|
|
1524
|
+
*/
|
|
1525
|
+
function useTrackEvent() {
|
|
1526
|
+
const { projectId, fetcher } = useOxyApp();
|
|
1527
|
+
const queueRef = react.useRef([]);
|
|
1528
|
+
const flushTimerRef = react.useRef(null);
|
|
1529
|
+
const flush = react.useCallback(() => {
|
|
1530
|
+
flushTimerRef.current = null;
|
|
1531
|
+
if (!projectId) return;
|
|
1532
|
+
const batch = queueRef.current;
|
|
1533
|
+
if (batch.length === 0) return;
|
|
1534
|
+
queueRef.current = [];
|
|
1535
|
+
for (const evt of batch) {
|
|
1536
|
+
const url = `/api/customer-apps/${projectId}/events`;
|
|
1537
|
+
const body = JSON.stringify(evt);
|
|
1538
|
+
try {
|
|
1539
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
1540
|
+
else fetcher(url, {
|
|
1541
|
+
method: "POST",
|
|
1542
|
+
headers: { "content-type": "application/json" },
|
|
1543
|
+
body
|
|
1544
|
+
}).catch((e) => {
|
|
1545
|
+
console.warn("[oxy] useTrackEvent flush failed:", e);
|
|
1546
|
+
});
|
|
1547
|
+
} catch (e) {
|
|
1548
|
+
console.warn("[oxy] useTrackEvent enqueue failed:", e);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}, [projectId, fetcher]);
|
|
1552
|
+
react.useEffect(() => {
|
|
1553
|
+
if (typeof window === "undefined") return;
|
|
1554
|
+
const onHide = () => flush();
|
|
1555
|
+
window.addEventListener("pagehide", onHide);
|
|
1556
|
+
return () => {
|
|
1557
|
+
window.removeEventListener("pagehide", onHide);
|
|
1558
|
+
flush();
|
|
1559
|
+
if (flushTimerRef.current !== null) {
|
|
1560
|
+
clearTimeout(flushTimerRef.current);
|
|
1561
|
+
flushTimerRef.current = null;
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
}, [flush]);
|
|
1565
|
+
return react.useCallback((name, payload) => {
|
|
1566
|
+
queueRef.current.push({
|
|
1567
|
+
event_name: name,
|
|
1568
|
+
payload: payload ?? {}
|
|
1569
|
+
});
|
|
1570
|
+
if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
|
|
1571
|
+
}, [flush]);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1169
1574
|
* Renders an agent run's answer + artifacts + thread link as a
|
|
1170
1575
|
* single block. The default styling is intentionally neutral
|
|
1171
1576
|
* (system fonts, gray surfaces) so it blends into any bundle.
|
|
@@ -1187,25 +1592,55 @@ function OxyAnswer(props) {
|
|
|
1187
1592
|
const isRunning = state === "running";
|
|
1188
1593
|
const isFailed = state === "failed";
|
|
1189
1594
|
const needsClarification = state === "needs_clarification";
|
|
1190
|
-
return /* @__PURE__ */
|
|
1595
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1191
1596
|
className,
|
|
1192
|
-
style: styles.answerWrap
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1597
|
+
style: styles.answerWrap,
|
|
1598
|
+
children: [
|
|
1599
|
+
isRunning && answer === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1600
|
+
style: styles.statusRow,
|
|
1601
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1602
|
+
style: styles.spinner,
|
|
1603
|
+
"aria-hidden": "true"
|
|
1604
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1605
|
+
style: styles.statusText,
|
|
1606
|
+
children: "Thinking…"
|
|
1607
|
+
})]
|
|
1608
|
+
}) : null,
|
|
1609
|
+
artifacts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1610
|
+
style: styles.artifactList,
|
|
1611
|
+
children: artifacts.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SqlArtifactBlock, {
|
|
1612
|
+
artifact: a,
|
|
1613
|
+
maxRows: maxArtifactRows
|
|
1614
|
+
}, a.id))
|
|
1615
|
+
}) : null,
|
|
1616
|
+
answer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1617
|
+
style: styles.markdown,
|
|
1618
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkdownText, { text: answer })
|
|
1619
|
+
}) : null,
|
|
1620
|
+
needsClarification && clarification ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1621
|
+
style: styles.clarification,
|
|
1622
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1623
|
+
style: { marginTop: 4 },
|
|
1624
|
+
children: clarification
|
|
1625
|
+
})]
|
|
1626
|
+
}) : null,
|
|
1627
|
+
isFailed && error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBlock, { error }) : null,
|
|
1628
|
+
threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1629
|
+
style: styles.threadLinkRow,
|
|
1630
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
1631
|
+
href: threadUrl,
|
|
1632
|
+
target: "_blank",
|
|
1633
|
+
rel: "noreferrer noopener",
|
|
1634
|
+
style: styles.threadLink,
|
|
1635
|
+
children: [threadLinkLabel, " →"]
|
|
1636
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1637
|
+
style: styles.betaBadge,
|
|
1638
|
+
title: "Thread linking is in beta — see docs",
|
|
1639
|
+
children: "beta"
|
|
1640
|
+
})]
|
|
1641
|
+
}) : null
|
|
1642
|
+
]
|
|
1643
|
+
});
|
|
1209
1644
|
}
|
|
1210
1645
|
/**
|
|
1211
1646
|
* Complete drop-in chat surface. One agent, one input, one answer
|
|
@@ -1229,37 +1664,48 @@ function OxyChat(props) {
|
|
|
1229
1664
|
if (!q || run.state === "running") return;
|
|
1230
1665
|
run.ask(q);
|
|
1231
1666
|
}, [question, run]);
|
|
1232
|
-
return /* @__PURE__ */
|
|
1667
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1233
1668
|
className,
|
|
1234
|
-
style: styles.chatWrap
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1669
|
+
style: styles.chatWrap,
|
|
1670
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1671
|
+
onSubmit: submit,
|
|
1672
|
+
style: styles.chatForm,
|
|
1673
|
+
children: [
|
|
1674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1675
|
+
type: "text",
|
|
1676
|
+
value: question,
|
|
1677
|
+
onChange: (e) => setQuestion(e.target.value),
|
|
1678
|
+
placeholder,
|
|
1679
|
+
disabled: run.state === "running",
|
|
1680
|
+
style: styles.chatInput,
|
|
1681
|
+
"aria-label": "Question"
|
|
1682
|
+
}),
|
|
1683
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1684
|
+
type: "submit",
|
|
1685
|
+
disabled: run.state === "running" || question.trim() === "",
|
|
1686
|
+
style: styles.chatSubmit,
|
|
1687
|
+
children: run.state === "running" ? "…" : submitLabel
|
|
1688
|
+
}),
|
|
1689
|
+
run.state === "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1690
|
+
type: "button",
|
|
1691
|
+
onClick: run.cancel,
|
|
1692
|
+
style: styles.chatCancel,
|
|
1693
|
+
children: "Stop"
|
|
1694
|
+
}) : null
|
|
1695
|
+
]
|
|
1696
|
+
}), run.state === "idle" ? emptyState ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1697
|
+
style: styles.emptyState,
|
|
1698
|
+
children: "Ask a question to get started."
|
|
1699
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAnswer, {
|
|
1700
|
+
answer: run.answer,
|
|
1701
|
+
artifacts: run.artifacts,
|
|
1702
|
+
state: run.state,
|
|
1703
|
+
clarification: run.clarification,
|
|
1704
|
+
error: run.error,
|
|
1705
|
+
threadUrl: run.threadUrl,
|
|
1706
|
+
maxArtifactRows
|
|
1707
|
+
})]
|
|
1708
|
+
});
|
|
1263
1709
|
}
|
|
1264
1710
|
function SqlArtifactBlock(props) {
|
|
1265
1711
|
const { artifact, maxRows } = props;
|
|
@@ -1268,17 +1714,53 @@ function SqlArtifactBlock(props) {
|
|
|
1268
1714
|
const truncated = results ? results.rows.length > maxRows : false;
|
|
1269
1715
|
const visibleRows = results ? results.rows.slice(0, maxRows) : [];
|
|
1270
1716
|
const sourceLabel = artifact.source === "verified_sql" ? "Verified query" : artifact.source === "semantic_query" ? "Semantic query" : artifact.source === "omni_query" ? "Omni query" : "Query";
|
|
1271
|
-
return /* @__PURE__ */
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1717
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1718
|
+
style: styles.artifact,
|
|
1719
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1720
|
+
type: "button",
|
|
1721
|
+
onClick: () => setOpen((o) => !o),
|
|
1722
|
+
style: styles.artifactHeader,
|
|
1723
|
+
children: [
|
|
1724
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1725
|
+
style: styles.artifactBadge,
|
|
1726
|
+
children: sourceLabel
|
|
1727
|
+
}),
|
|
1728
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1729
|
+
style: styles.artifactSummary,
|
|
1730
|
+
children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
|
|
1731
|
+
}),
|
|
1732
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1733
|
+
style: styles.artifactToggle,
|
|
1734
|
+
children: open ? "Hide" : "Show"
|
|
1735
|
+
})
|
|
1736
|
+
]
|
|
1737
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1738
|
+
style: styles.sqlBlock,
|
|
1739
|
+
children: artifact.sql
|
|
1740
|
+
}), artifact.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1741
|
+
style: styles.error,
|
|
1742
|
+
children: artifact.error
|
|
1743
|
+
}) : results ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1744
|
+
style: styles.resultsWrap,
|
|
1745
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
1746
|
+
style: styles.resultsTable,
|
|
1747
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: results.columns.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
1748
|
+
style: styles.resultsTh,
|
|
1749
|
+
children: c
|
|
1750
|
+
}, c)) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: row.map((cell, j) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1751
|
+
style: styles.resultsTd,
|
|
1752
|
+
children: formatCell(cell)
|
|
1753
|
+
}, j)) }, i)) })]
|
|
1754
|
+
}), truncated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1755
|
+
style: styles.truncatedNote,
|
|
1756
|
+
children: [
|
|
1757
|
+
"+",
|
|
1758
|
+
results.rows.length - maxRows,
|
|
1759
|
+
" more rows. Open the thread in Oxy to see all."
|
|
1760
|
+
]
|
|
1761
|
+
}) : null]
|
|
1762
|
+
}) : null] }) : null]
|
|
1763
|
+
});
|
|
1282
1764
|
}
|
|
1283
1765
|
function formatCell(value) {
|
|
1284
1766
|
if (value === null || value === void 0) return "—";
|
|
@@ -1293,16 +1775,36 @@ function formatCell(value) {
|
|
|
1293
1775
|
*/
|
|
1294
1776
|
function ErrorBlock(props) {
|
|
1295
1777
|
const { error } = props;
|
|
1296
|
-
if (error instanceof OxyApiError) return /* @__PURE__ */
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1778
|
+
if (error instanceof OxyApiError) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1779
|
+
style: styles.error,
|
|
1780
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
1781
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
|
|
1782
|
+
" ",
|
|
1783
|
+
error.message.split("\n\n")[0]
|
|
1784
|
+
] }), error.hint ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1785
|
+
style: {
|
|
1786
|
+
marginTop: 6,
|
|
1787
|
+
fontWeight: 400,
|
|
1788
|
+
whiteSpace: "pre-wrap"
|
|
1789
|
+
},
|
|
1790
|
+
children: [
|
|
1791
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Hint:" }),
|
|
1792
|
+
" ",
|
|
1793
|
+
error.hint
|
|
1794
|
+
]
|
|
1795
|
+
}) : null]
|
|
1796
|
+
});
|
|
1797
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1798
|
+
style: styles.error,
|
|
1799
|
+
children: [
|
|
1800
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
|
|
1801
|
+
" ",
|
|
1802
|
+
error.message
|
|
1803
|
+
]
|
|
1804
|
+
});
|
|
1302
1805
|
}
|
|
1303
1806
|
function MarkdownText(props) {
|
|
1304
|
-
|
|
1305
|
-
return /* @__PURE__ */ react.createElement(react.Fragment, null, blocks);
|
|
1807
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: react.useMemo(() => parseMarkdown(props.text), [props.text]) });
|
|
1306
1808
|
}
|
|
1307
1809
|
function parseMarkdown(text) {
|
|
1308
1810
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
@@ -1372,27 +1874,23 @@ function parseMarkdown(text) {
|
|
|
1372
1874
|
}
|
|
1373
1875
|
return blocks.map((b, idx) => {
|
|
1374
1876
|
switch (b.kind) {
|
|
1375
|
-
case "h": {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
style: headingStyle
|
|
1381
|
-
}, renderInline(b.text));
|
|
1382
|
-
}
|
|
1383
|
-
case "code": return /* @__PURE__ */ react.createElement("pre", {
|
|
1384
|
-
key: idx,
|
|
1877
|
+
case "h": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(`h${b.level}`, {
|
|
1878
|
+
style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
|
|
1879
|
+
children: renderInline(b.text)
|
|
1880
|
+
}, idx);
|
|
1881
|
+
case "code": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1385
1882
|
style: styles.codeBlock,
|
|
1386
|
-
"data-lang": b.lang || void 0
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
style: styles.list
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
style: styles.paragraph
|
|
1395
|
-
|
|
1883
|
+
"data-lang": b.lang || void 0,
|
|
1884
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: b.code })
|
|
1885
|
+
}, idx);
|
|
1886
|
+
case "list": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1887
|
+
style: styles.list,
|
|
1888
|
+
children: b.items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: renderInline(item) }, i))
|
|
1889
|
+
}, idx);
|
|
1890
|
+
case "p": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1891
|
+
style: styles.paragraph,
|
|
1892
|
+
children: renderInline(b.text)
|
|
1893
|
+
}, idx);
|
|
1396
1894
|
}
|
|
1397
1895
|
});
|
|
1398
1896
|
}
|
|
@@ -1408,27 +1906,31 @@ function renderInline(text) {
|
|
|
1408
1906
|
const patterns = [
|
|
1409
1907
|
{
|
|
1410
1908
|
re: /`([^`]+)`/,
|
|
1411
|
-
render: (m) => /* @__PURE__ */
|
|
1909
|
+
render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1910
|
+
style: styles.inlineCode,
|
|
1911
|
+
children: m[1]
|
|
1912
|
+
})
|
|
1412
1913
|
},
|
|
1413
1914
|
{
|
|
1414
1915
|
re: /\[([^\]]+)\]\(([^)]+)\)/,
|
|
1415
1916
|
render: (m) => {
|
|
1416
|
-
if (isSafeLinkHref(m[2])) return /* @__PURE__ */
|
|
1917
|
+
if (isSafeLinkHref(m[2])) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1417
1918
|
href: m[2],
|
|
1418
1919
|
target: "_blank",
|
|
1419
1920
|
rel: "noreferrer noopener",
|
|
1420
|
-
style: styles.link
|
|
1421
|
-
|
|
1422
|
-
|
|
1921
|
+
style: styles.link,
|
|
1922
|
+
children: m[1]
|
|
1923
|
+
});
|
|
1924
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: m[1] });
|
|
1423
1925
|
}
|
|
1424
1926
|
},
|
|
1425
1927
|
{
|
|
1426
1928
|
re: /\*\*([^*]+)\*\*/,
|
|
1427
|
-
render: (m) => /* @__PURE__ */
|
|
1929
|
+
render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: m[1] })
|
|
1428
1930
|
},
|
|
1429
1931
|
{
|
|
1430
1932
|
re: /\*([^*]+)\*/,
|
|
1431
|
-
render: (m) => /* @__PURE__ */
|
|
1933
|
+
render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: m[1] })
|
|
1432
1934
|
}
|
|
1433
1935
|
];
|
|
1434
1936
|
while (remaining.length > 0) {
|
|
@@ -1446,7 +1948,7 @@ function renderInline(text) {
|
|
|
1446
1948
|
break;
|
|
1447
1949
|
}
|
|
1448
1950
|
if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
|
|
1449
|
-
segments.push(/* @__PURE__ */
|
|
1951
|
+
segments.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: earliest.node }, key++));
|
|
1450
1952
|
remaining = remaining.slice(earliest.idx + earliest.len);
|
|
1451
1953
|
}
|
|
1452
1954
|
return segments;
|
|
@@ -1676,6 +2178,136 @@ const styles = {
|
|
|
1676
2178
|
};
|
|
1677
2179
|
|
|
1678
2180
|
//#endregion
|
|
2181
|
+
//#region src/metricTree.ts
|
|
2182
|
+
/**
|
|
2183
|
+
* Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
|
|
2184
|
+
* airlayer metric-tree analyses (tree introspection, sensitivity, predict,
|
|
2185
|
+
* explain, opportunity) over typed methods.
|
|
2186
|
+
*
|
|
2187
|
+
* Construction is internal to {@link OxyClient} — call `client.metricTree`
|
|
2188
|
+
* to access an instance rather than building one yourself.
|
|
2189
|
+
*
|
|
2190
|
+
* @example
|
|
2191
|
+
* ```typescript
|
|
2192
|
+
* const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
|
|
2193
|
+
* const tree = await client.metricTree.getTree();
|
|
2194
|
+
* const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2195
|
+
* ```
|
|
2196
|
+
*/
|
|
2197
|
+
var MetricTreeClient = class {
|
|
2198
|
+
constructor(config, request) {
|
|
2199
|
+
this.config = config;
|
|
2200
|
+
this.request = request;
|
|
2201
|
+
}
|
|
2202
|
+
path(suffix) {
|
|
2203
|
+
return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
|
|
2204
|
+
}
|
|
2205
|
+
buildQuery(extra = {}) {
|
|
2206
|
+
const params = { ...extra };
|
|
2207
|
+
if (this.config.branch) params.branch = this.config.branch;
|
|
2208
|
+
const qs = new URLSearchParams(params).toString();
|
|
2209
|
+
return qs ? `?${qs}` : "";
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Fetch the full metric tree, or the subtree rooted at `root`.
|
|
2213
|
+
*
|
|
2214
|
+
* @param root - Optional fully-qualified measure id to root the tree at.
|
|
2215
|
+
* @returns Nodes (measures) and edges (component / driver relationships).
|
|
2216
|
+
*
|
|
2217
|
+
* @example
|
|
2218
|
+
* ```typescript
|
|
2219
|
+
* const tree = await client.metricTree.getTree();
|
|
2220
|
+
* const subtree = await client.metricTree.getTree("orders.net_revenue");
|
|
2221
|
+
* ```
|
|
2222
|
+
*/
|
|
2223
|
+
async getTree(root) {
|
|
2224
|
+
const query = this.buildQuery(root ? { root } : {});
|
|
2225
|
+
return this.request(this.path(query));
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Rank the declared drivers of a measure by influence.
|
|
2229
|
+
*
|
|
2230
|
+
* @param measureId - Fully-qualified measure id (`view.measure`).
|
|
2231
|
+
*
|
|
2232
|
+
* @example
|
|
2233
|
+
* ```typescript
|
|
2234
|
+
* const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2235
|
+
* for (const driver of sensitivity.drivers) {
|
|
2236
|
+
* console.log(driver.measure, driver.direction, driver.strength);
|
|
2237
|
+
* }
|
|
2238
|
+
* ```
|
|
2239
|
+
*/
|
|
2240
|
+
async getSensitivity(measureId) {
|
|
2241
|
+
const query = this.buildQuery();
|
|
2242
|
+
return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Propagate hypothetical `(measure, delta)` changes upward through the
|
|
2246
|
+
* tree. Returns the estimated impact on every downstream measure.
|
|
2247
|
+
*
|
|
2248
|
+
* @example
|
|
2249
|
+
* ```typescript
|
|
2250
|
+
* const result = await client.metricTree.predict([
|
|
2251
|
+
* { measure: "marketing_spend.total_spend", delta: 10000 },
|
|
2252
|
+
* ]);
|
|
2253
|
+
* ```
|
|
2254
|
+
*/
|
|
2255
|
+
async predict(changes) {
|
|
2256
|
+
const query = this.buildQuery();
|
|
2257
|
+
return this.request(this.path(`/predict${query}`), {
|
|
2258
|
+
method: "POST",
|
|
2259
|
+
body: JSON.stringify({ changes })
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Period-over-period root-cause decomposition. Recursively splits the
|
|
2264
|
+
* target measure by components and dimensions until the move concentrates.
|
|
2265
|
+
*
|
|
2266
|
+
* @example
|
|
2267
|
+
* ```typescript
|
|
2268
|
+
* const result = await client.metricTree.explain({
|
|
2269
|
+
* target: "financials.operating_profit",
|
|
2270
|
+
* time_dimension: "financials.month",
|
|
2271
|
+
* current_period: ["2025-09-01", "2025-09-30"],
|
|
2272
|
+
* previous_period: ["2025-08-01", "2025-08-31"],
|
|
2273
|
+
* });
|
|
2274
|
+
* ```
|
|
2275
|
+
*/
|
|
2276
|
+
async explain(request) {
|
|
2277
|
+
const query = this.buildQuery();
|
|
2278
|
+
return this.request(this.path(`/explain${query}`), {
|
|
2279
|
+
method: "POST",
|
|
2280
|
+
body: JSON.stringify(request)
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Size the upside opportunity for a measure by finding underperforming
|
|
2285
|
+
* segments. Skips high-cardinality dimensions and trims the long tail.
|
|
2286
|
+
*
|
|
2287
|
+
* @example
|
|
2288
|
+
* ```typescript
|
|
2289
|
+
* const result = await client.metricTree.findOpportunities({
|
|
2290
|
+
* target: "orders.net_revenue",
|
|
2291
|
+
* time_dimension: "orders.order_date",
|
|
2292
|
+
* period: ["2025-09-01", "2025-09-30"],
|
|
2293
|
+
* });
|
|
2294
|
+
* for (const dim of result.dimensions) {
|
|
2295
|
+
* console.log(dim.dimension, "+", dim.total_upside);
|
|
2296
|
+
* }
|
|
2297
|
+
* ```
|
|
2298
|
+
*/
|
|
2299
|
+
async findOpportunities(request) {
|
|
2300
|
+
const query = this.buildQuery();
|
|
2301
|
+
return this.request(this.path(`/opportunity${query}`), {
|
|
2302
|
+
method: "POST",
|
|
2303
|
+
body: JSON.stringify(request)
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
//#endregion
|
|
2309
|
+
exports.AnomaliesClient = AnomaliesClient;
|
|
2310
|
+
exports.MetricTreeClient = MetricTreeClient;
|
|
1679
2311
|
exports.OxyAnswer = OxyAnswer;
|
|
1680
2312
|
exports.OxyApiError = OxyApiError;
|
|
1681
2313
|
exports.OxyAppProvider = OxyAppProvider;
|
|
@@ -1688,8 +2320,10 @@ exports.loadCustomerAppManifest = loadCustomerAppManifest;
|
|
|
1688
2320
|
exports.readInjectedAppConfig = readInjectedAppConfig;
|
|
1689
2321
|
exports.setOxyAppLogger = setOxyAppLogger;
|
|
1690
2322
|
exports.useAgentRun = useAgentRun;
|
|
2323
|
+
exports.useFunction = useFunction;
|
|
1691
2324
|
exports.useProcedureRun = useProcedureRun;
|
|
1692
2325
|
exports.useQuery = useQuery;
|
|
1693
2326
|
exports.useResolvedManifest = useResolvedManifest;
|
|
1694
2327
|
exports.useSemanticQuery = useSemanticQuery;
|
|
2328
|
+
exports.useTrackEvent = useTrackEvent;
|
|
1695
2329
|
//# sourceMappingURL=index.cjs.map
|