@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.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. */
|
|
@@ -94,6 +176,52 @@ async function getCustomerAppDebug(resolved) {
|
|
|
94
176
|
|
|
95
177
|
//#endregion
|
|
96
178
|
//#region src/customer-app/errors.ts
|
|
179
|
+
/**
|
|
180
|
+
* Error thrown by all customer-app hooks when an API call returns a
|
|
181
|
+
* non-2xx response. Carries the structured `code` + `hint` the server
|
|
182
|
+
* emits so bundle UIs can render an actionable message instead of
|
|
183
|
+
* "404: { ...json... }".
|
|
184
|
+
*/
|
|
185
|
+
var OxyApiError = class extends Error {
|
|
186
|
+
constructor(opts) {
|
|
187
|
+
const base = opts.message || `HTTP ${opts.status}`;
|
|
188
|
+
const code = opts.code ? ` [${opts.code}]` : "";
|
|
189
|
+
const hint = opts.hint ? `\n\n${opts.hint}` : "";
|
|
190
|
+
super(`${base}${code}${hint}`);
|
|
191
|
+
this.name = "OxyApiError";
|
|
192
|
+
this.status = opts.status;
|
|
193
|
+
this.code = opts.code ?? null;
|
|
194
|
+
this.hint = opts.hint ?? null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Read a non-2xx response from oxy and return an `OxyApiError`.
|
|
199
|
+
* Parses the JSON envelope when present; falls back to raw text
|
|
200
|
+
* (truncated to 240 chars so a runaway HTML error page doesn't
|
|
201
|
+
* dominate the bundle UI).
|
|
202
|
+
*/
|
|
203
|
+
async function apiErrorFromResponse(resp) {
|
|
204
|
+
let body;
|
|
205
|
+
let raw = "";
|
|
206
|
+
try {
|
|
207
|
+
raw = await resp.text();
|
|
208
|
+
body = raw ? JSON.parse(raw) : void 0;
|
|
209
|
+
} catch {}
|
|
210
|
+
if (body && typeof body === "object") {
|
|
211
|
+
const b = body;
|
|
212
|
+
return new OxyApiError({
|
|
213
|
+
status: resp.status,
|
|
214
|
+
message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
|
|
215
|
+
code: typeof b.code === "string" ? b.code : null,
|
|
216
|
+
hint: typeof b.hint === "string" ? b.hint : null
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
|
|
220
|
+
return new OxyApiError({
|
|
221
|
+
status: resp.status,
|
|
222
|
+
message: snippet || `HTTP ${resp.status}`
|
|
223
|
+
});
|
|
224
|
+
}
|
|
97
225
|
const ARCH_DOC = "internal-docs/customer-apps.md";
|
|
98
226
|
/** Interpret a thrown error as a structured report for UI display. */
|
|
99
227
|
function interpretCustomerAppError(err) {
|
|
@@ -284,13 +412,174 @@ function validateManifest(raw, url) {
|
|
|
284
412
|
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
285
413
|
slug: raw.slug,
|
|
286
414
|
orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
|
|
287
|
-
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
|
|
415
|
+
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
|
|
416
|
+
functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
|
|
288
417
|
};
|
|
289
418
|
}
|
|
419
|
+
const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
420
|
+
/**
|
|
421
|
+
* Validate the optional `functions` map. Each key is a function name;
|
|
422
|
+
* each value declares how the function is invoked. Mirrors the
|
|
423
|
+
* server-side validation in `customer_apps_publish.rs` so a bad
|
|
424
|
+
* manifest fails at build, not at publish.
|
|
425
|
+
*/
|
|
426
|
+
function validateFunctions(raw) {
|
|
427
|
+
if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
|
|
428
|
+
const out = {};
|
|
429
|
+
for (const [fnName, value] of Object.entries(raw)) {
|
|
430
|
+
if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
|
|
431
|
+
if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
|
|
432
|
+
const fn = {};
|
|
433
|
+
if (value.entry !== void 0) {
|
|
434
|
+
if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
|
|
435
|
+
fn.entry = value.entry;
|
|
436
|
+
}
|
|
437
|
+
if (value.schedule !== void 0) {
|
|
438
|
+
if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
|
|
439
|
+
fn.schedule = value.schedule;
|
|
440
|
+
}
|
|
441
|
+
if (value.timezone !== void 0) {
|
|
442
|
+
if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
|
|
443
|
+
fn.timezone = value.timezone;
|
|
444
|
+
}
|
|
445
|
+
if (value.route !== void 0) {
|
|
446
|
+
if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
|
|
447
|
+
fn.route = value.route;
|
|
448
|
+
}
|
|
449
|
+
if (value.airwayStep !== void 0) {
|
|
450
|
+
const step = value.airwayStep;
|
|
451
|
+
if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
|
|
452
|
+
fn.airwayStep = {
|
|
453
|
+
pipeline: step.pipeline,
|
|
454
|
+
resource: step.resource
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (value.timeoutSeconds !== void 0) {
|
|
458
|
+
const t = value.timeoutSeconds;
|
|
459
|
+
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]`);
|
|
460
|
+
fn.timeoutSeconds = t;
|
|
461
|
+
}
|
|
462
|
+
if (value.cache !== void 0) {
|
|
463
|
+
const c = value.cache;
|
|
464
|
+
if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
|
|
465
|
+
if (c.ttlSeconds !== void 0) {
|
|
466
|
+
const ttl = c.ttlSeconds;
|
|
467
|
+
if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
|
|
468
|
+
fn.cache = { ttlSeconds: ttl };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (value.retries !== void 0) {
|
|
472
|
+
const r = value.retries;
|
|
473
|
+
if (!isRecord(r)) throw new Error(`oxy-app.json: function "${fnName}" \`retries\` must be an object`);
|
|
474
|
+
const retries = {};
|
|
475
|
+
for (const key of [
|
|
476
|
+
"maxAttempts",
|
|
477
|
+
"minTimeoutMs",
|
|
478
|
+
"maxTimeoutMs"
|
|
479
|
+
]) {
|
|
480
|
+
const n = r[key];
|
|
481
|
+
if (n !== void 0) {
|
|
482
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw new Error(`oxy-app.json: function "${fnName}" \`retries.${key}\` must be a positive integer`);
|
|
483
|
+
retries[key] = n;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
fn.retries = retries;
|
|
487
|
+
}
|
|
488
|
+
if (value.inputExample !== void 0) fn.inputExample = value.inputExample;
|
|
489
|
+
const hasSchedule = fn.schedule !== void 0;
|
|
490
|
+
const hasAirway = fn.airwayStep !== void 0;
|
|
491
|
+
if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
|
|
492
|
+
out[fnName] = fn;
|
|
493
|
+
}
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
290
496
|
function isRecord(v) {
|
|
291
497
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
292
498
|
}
|
|
293
499
|
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/customer-app/function-invoke.ts
|
|
502
|
+
const inflight$1 = /* @__PURE__ */ new Map();
|
|
503
|
+
/**
|
|
504
|
+
* Dedup key for an invocation: function name + its (stable-serialized) body,
|
|
505
|
+
* joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
|
|
506
|
+
* the separator can never collide with a name.
|
|
507
|
+
*/
|
|
508
|
+
function functionInvokeKey(name, body) {
|
|
509
|
+
return `${name}\n${JSON.stringify(body ?? {})}`;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Run `run()` unless an identical invocation is already in flight, in which
|
|
513
|
+
* case share its promise. The entry is removed once the promise settles — so
|
|
514
|
+
* this dedups concurrency only, it does NOT memoize the result.
|
|
515
|
+
*/
|
|
516
|
+
function sharedFunctionInvoke(key, run) {
|
|
517
|
+
const existing = inflight$1.get(key);
|
|
518
|
+
if (existing) return existing;
|
|
519
|
+
const p = run().finally(() => {
|
|
520
|
+
inflight$1.delete(key);
|
|
521
|
+
});
|
|
522
|
+
inflight$1.set(key, p);
|
|
523
|
+
return p;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/customer-app/function-sse.ts
|
|
528
|
+
/**
|
|
529
|
+
* Read a `text/event-stream` function response to completion. Resolves with the
|
|
530
|
+
* decoded result + captured logs, or rejects (with `.logs` attached) on an
|
|
531
|
+
* `event: error` frame / a stream that ends without a terminal event.
|
|
532
|
+
*/
|
|
533
|
+
async function readFunctionSseStream(resp) {
|
|
534
|
+
const reader = resp.body?.getReader();
|
|
535
|
+
if (!reader) throw new Error("function response has no body stream");
|
|
536
|
+
const decoder = new TextDecoder();
|
|
537
|
+
let buffer = "";
|
|
538
|
+
let dataPayload = "";
|
|
539
|
+
const logs = [];
|
|
540
|
+
const handleFrame = (frame) => {
|
|
541
|
+
let event = "message";
|
|
542
|
+
let data = "";
|
|
543
|
+
for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
544
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
545
|
+
if (event === "log") try {
|
|
546
|
+
const l = JSON.parse(data);
|
|
547
|
+
logs.push({
|
|
548
|
+
level: String(l.level ?? "info"),
|
|
549
|
+
message: String(l.message ?? "")
|
|
550
|
+
});
|
|
551
|
+
} catch {}
|
|
552
|
+
else if (event === "data") dataPayload = data;
|
|
553
|
+
else if (event === "done") return {
|
|
554
|
+
done: true,
|
|
555
|
+
value: dataPayload ? JSON.parse(dataPayload) : null
|
|
556
|
+
};
|
|
557
|
+
else if (event === "error") {
|
|
558
|
+
const payload = data ? JSON.parse(data) : {};
|
|
559
|
+
const err = new Error(payload.message || payload.error || "function invocation failed");
|
|
560
|
+
err.name = payload.error || "FunctionError";
|
|
561
|
+
err.logs = logs;
|
|
562
|
+
throw err;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
for (;;) {
|
|
566
|
+
const { done, value } = await reader.read();
|
|
567
|
+
if (done) break;
|
|
568
|
+
buffer += decoder.decode(value, { stream: true });
|
|
569
|
+
let sep;
|
|
570
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
571
|
+
const frame = buffer.slice(0, sep);
|
|
572
|
+
buffer = buffer.slice(sep + 2);
|
|
573
|
+
const result = handleFrame(frame);
|
|
574
|
+
if (result) return {
|
|
575
|
+
value: result.value,
|
|
576
|
+
logs
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
throw new Error("function stream ended without a terminal event");
|
|
581
|
+
}
|
|
582
|
+
|
|
294
583
|
//#endregion
|
|
295
584
|
//#region src/customer-app/interpolate.ts
|
|
296
585
|
/**
|
|
@@ -355,6 +644,50 @@ function isSafeLinkHref(raw) {
|
|
|
355
644
|
return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
|
|
356
645
|
}
|
|
357
646
|
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/customer-app/query-cache.ts
|
|
649
|
+
const SWR_TTL_MS = 3e4;
|
|
650
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
651
|
+
const cache = /* @__PURE__ */ new Map();
|
|
652
|
+
function queryKey(projectId, db, sql) {
|
|
653
|
+
return `${projectId} ${db ?? ""} ${sql}`;
|
|
654
|
+
}
|
|
655
|
+
function getCached(projectId, sql, db) {
|
|
656
|
+
const e = cache.get(queryKey(projectId, db, sql));
|
|
657
|
+
return e && Date.now() - e.at < SWR_TTL_MS ? e.data : void 0;
|
|
658
|
+
}
|
|
659
|
+
/** Fetch with in-flight dedup + cache. `force` bypasses the fresh-cache
|
|
660
|
+
* short-circuit (used by refetch) but still dedupes a concurrent in-flight. */
|
|
661
|
+
async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
|
|
662
|
+
const key = queryKey(projectId, db, sql);
|
|
663
|
+
if (!opts.force) {
|
|
664
|
+
const fresh = getCached(projectId, sql, db);
|
|
665
|
+
if (fresh) return fresh;
|
|
666
|
+
}
|
|
667
|
+
const existing = inflight.get(key);
|
|
668
|
+
if (existing) return existing;
|
|
669
|
+
const body = JSON.stringify({
|
|
670
|
+
sql,
|
|
671
|
+
...db ? { database: db } : {}
|
|
672
|
+
});
|
|
673
|
+
const p = (async () => {
|
|
674
|
+
const resp = await fetcher(`/api/projects/${projectId}/query`, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "content-type": "application/json" },
|
|
677
|
+
body
|
|
678
|
+
});
|
|
679
|
+
if (!resp.ok) throw await apiErrorFromResponse(resp);
|
|
680
|
+
const data = await resp.json();
|
|
681
|
+
cache.set(key, {
|
|
682
|
+
at: Date.now(),
|
|
683
|
+
data
|
|
684
|
+
});
|
|
685
|
+
return data;
|
|
686
|
+
})().finally(() => inflight.delete(key));
|
|
687
|
+
inflight.set(key, p);
|
|
688
|
+
return p;
|
|
689
|
+
}
|
|
690
|
+
|
|
358
691
|
//#endregion
|
|
359
692
|
//#region src/customer-app/react.tsx
|
|
360
693
|
function defaultFetcher(input, init) {
|
|
@@ -396,26 +729,55 @@ function OxyAppProvider(props) {
|
|
|
396
729
|
}, [manifestOptions, fetcher]);
|
|
397
730
|
if (state.status === "error" && state.error) {
|
|
398
731
|
const err = state.error;
|
|
399
|
-
return /* @__PURE__ */
|
|
732
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
|
|
733
|
+
value: state,
|
|
734
|
+
children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
|
|
735
|
+
});
|
|
400
736
|
}
|
|
401
|
-
if (state.status === "loading") return /* @__PURE__ */
|
|
402
|
-
|
|
737
|
+
if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
|
|
738
|
+
value: state,
|
|
739
|
+
children: fallback ?? null
|
|
740
|
+
});
|
|
741
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
|
|
742
|
+
value: state,
|
|
743
|
+
children
|
|
744
|
+
});
|
|
403
745
|
}
|
|
404
746
|
function defaultErrorFallback(err) {
|
|
405
|
-
return /* @__PURE__ */
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
747
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
748
|
+
style: {
|
|
749
|
+
margin: "2rem auto",
|
|
750
|
+
maxWidth: "640px",
|
|
751
|
+
padding: "1rem",
|
|
752
|
+
border: "1px solid #fca5a5",
|
|
753
|
+
background: "#fee2e2",
|
|
754
|
+
color: "#991b1b",
|
|
755
|
+
borderRadius: "8px",
|
|
756
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
757
|
+
fontSize: "14px"
|
|
758
|
+
},
|
|
759
|
+
children: [
|
|
760
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
761
|
+
style: { fontWeight: 600 },
|
|
762
|
+
children: err.title
|
|
763
|
+
}),
|
|
764
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
765
|
+
style: {
|
|
766
|
+
fontSize: "12px",
|
|
767
|
+
marginTop: "4px"
|
|
768
|
+
},
|
|
769
|
+
children: err.message
|
|
770
|
+
}),
|
|
771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
772
|
+
style: { marginTop: "12px" },
|
|
773
|
+
children: [
|
|
774
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "What to try:" }),
|
|
775
|
+
" ",
|
|
776
|
+
err.hint
|
|
777
|
+
]
|
|
778
|
+
})
|
|
779
|
+
]
|
|
780
|
+
});
|
|
419
781
|
}
|
|
420
782
|
/**
|
|
421
783
|
* Emit a one-time `console.warn` the first time a beta hook is
|
|
@@ -430,58 +792,6 @@ function warnBetaOnce(name) {
|
|
|
430
792
|
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.`);
|
|
431
793
|
}
|
|
432
794
|
/**
|
|
433
|
-
* Error thrown by all customer-app hooks when an API call returns a
|
|
434
|
-
* non-2xx response. Carries the structured `code` + `hint` the server
|
|
435
|
-
* emits so bundle UIs can render an actionable message instead of
|
|
436
|
-
* "404: { ...json... }".
|
|
437
|
-
*
|
|
438
|
-
* The server contract is documented in
|
|
439
|
-
* `crates/app/src/server/api/projects/agent_ask.rs` and
|
|
440
|
-
* `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
|
|
441
|
-
* Hooks that previously wrapped the raw text in `new Error()` now
|
|
442
|
-
* throw this type instead.
|
|
443
|
-
*/
|
|
444
|
-
var OxyApiError = class extends Error {
|
|
445
|
-
constructor(opts) {
|
|
446
|
-
const base = opts.message || `HTTP ${opts.status}`;
|
|
447
|
-
const code = opts.code ? ` [${opts.code}]` : "";
|
|
448
|
-
const hint = opts.hint ? `\n\n${opts.hint}` : "";
|
|
449
|
-
super(`${base}${code}${hint}`);
|
|
450
|
-
this.name = "OxyApiError";
|
|
451
|
-
this.status = opts.status;
|
|
452
|
-
this.code = opts.code ?? null;
|
|
453
|
-
this.hint = opts.hint ?? null;
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
/**
|
|
457
|
-
* Read a non-2xx response from oxy and return an `OxyApiError`.
|
|
458
|
-
* Parses the JSON envelope when present; falls back to raw text
|
|
459
|
-
* (truncated to 240 chars so a runaway HTML error page doesn't
|
|
460
|
-
* dominate the bundle UI).
|
|
461
|
-
*/
|
|
462
|
-
async function apiErrorFromResponse(resp) {
|
|
463
|
-
let body;
|
|
464
|
-
let raw = "";
|
|
465
|
-
try {
|
|
466
|
-
raw = await resp.text();
|
|
467
|
-
body = raw ? JSON.parse(raw) : void 0;
|
|
468
|
-
} catch {}
|
|
469
|
-
if (body && typeof body === "object") {
|
|
470
|
-
const b = body;
|
|
471
|
-
return new OxyApiError({
|
|
472
|
-
status: resp.status,
|
|
473
|
-
message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
|
|
474
|
-
code: typeof b.code === "string" ? b.code : null,
|
|
475
|
-
hint: typeof b.hint === "string" ? b.hint : null
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
|
|
479
|
-
return new OxyApiError({
|
|
480
|
-
status: resp.status,
|
|
481
|
-
message: snippet || `HTTP ${resp.status}`
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
795
|
* Read the resolved manifest from context. Throws if called outside
|
|
486
796
|
* `<OxyAppProvider>` — that's a programmer error worth surfacing
|
|
487
797
|
* loudly, not silently swallowing.
|
|
@@ -535,36 +845,35 @@ function useQuery(input, opts = {}) {
|
|
|
535
845
|
} : s);
|
|
536
846
|
return;
|
|
537
847
|
}
|
|
538
|
-
const ctrl = new AbortController();
|
|
539
848
|
let cancelled = false;
|
|
849
|
+
const cached = getCached(projectId, sqlWithParams, input.database);
|
|
850
|
+
if (cached && nonce === 0) {
|
|
851
|
+
const { columns, rows } = cached;
|
|
852
|
+
const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
|
|
853
|
+
setState({
|
|
854
|
+
rows: objects,
|
|
855
|
+
columns,
|
|
856
|
+
loading: false,
|
|
857
|
+
error: null
|
|
858
|
+
});
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
540
861
|
setState((s) => ({
|
|
541
862
|
...s,
|
|
542
863
|
loading: true,
|
|
543
864
|
error: null
|
|
544
865
|
}));
|
|
545
|
-
|
|
546
|
-
sql: sqlWithParams,
|
|
547
|
-
...input.database ? { database: input.database } : {}
|
|
548
|
-
});
|
|
549
|
-
fetcher(`/api/projects/${projectId}/query`, {
|
|
550
|
-
method: "POST",
|
|
551
|
-
headers: { "content-type": "application/json" },
|
|
552
|
-
body,
|
|
553
|
-
signal: ctrl.signal
|
|
554
|
-
}).then(async (resp) => {
|
|
555
|
-
if (!resp.ok) throw await apiErrorFromResponse(resp);
|
|
556
|
-
return resp.json();
|
|
557
|
-
}).then(({ columns, rows }) => {
|
|
866
|
+
sharedQuery(fetcher, projectId, sqlWithParams, input.database, { force: nonce > 0 }).then(({ columns, rows }) => {
|
|
558
867
|
if (cancelled) return;
|
|
868
|
+
const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
|
|
559
869
|
setState({
|
|
560
|
-
rows:
|
|
870
|
+
rows: objects,
|
|
561
871
|
columns,
|
|
562
872
|
loading: false,
|
|
563
873
|
error: null
|
|
564
874
|
});
|
|
565
875
|
}).catch((err) => {
|
|
566
876
|
if (cancelled) return;
|
|
567
|
-
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
568
877
|
setState((s) => ({
|
|
569
878
|
...s,
|
|
570
879
|
loading: false,
|
|
@@ -573,7 +882,6 @@ function useQuery(input, opts = {}) {
|
|
|
573
882
|
});
|
|
574
883
|
return () => {
|
|
575
884
|
cancelled = true;
|
|
576
|
-
ctrl.abort();
|
|
577
885
|
};
|
|
578
886
|
}, [
|
|
579
887
|
enabled,
|
|
@@ -592,6 +900,81 @@ function useQuery(input, opts = {}) {
|
|
|
592
900
|
};
|
|
593
901
|
}
|
|
594
902
|
/**
|
|
903
|
+
* Imperative hook for invoking an Oxy Function by name.
|
|
904
|
+
*
|
|
905
|
+
* ```tsx
|
|
906
|
+
* const refresh = useFunction("refresh-sales");
|
|
907
|
+
* <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
|
|
908
|
+
* Refresh
|
|
909
|
+
* </button>
|
|
910
|
+
* ```
|
|
911
|
+
*/
|
|
912
|
+
function useFunction(name) {
|
|
913
|
+
const ctx = react.useContext(OxyAppContext);
|
|
914
|
+
if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
|
|
915
|
+
const fetcher = ctx.fetcher;
|
|
916
|
+
const resolved = ctx.resolved;
|
|
917
|
+
const [state, setState] = react.useState({
|
|
918
|
+
data: null,
|
|
919
|
+
isLoading: false,
|
|
920
|
+
error: null,
|
|
921
|
+
logs: []
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
invoke: react.useCallback(async (body, opts) => {
|
|
925
|
+
if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
|
|
926
|
+
const { orgSlug, appSlug, apiBaseUrl } = resolved;
|
|
927
|
+
const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
|
|
928
|
+
setState((s) => ({
|
|
929
|
+
...s,
|
|
930
|
+
isLoading: true,
|
|
931
|
+
error: null
|
|
932
|
+
}));
|
|
933
|
+
try {
|
|
934
|
+
const headers = {
|
|
935
|
+
"content-type": "application/json",
|
|
936
|
+
accept: "text/event-stream"
|
|
937
|
+
};
|
|
938
|
+
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
939
|
+
const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
|
|
940
|
+
const resp = await fetcher(url, {
|
|
941
|
+
method: "POST",
|
|
942
|
+
headers,
|
|
943
|
+
body: JSON.stringify(body ?? {})
|
|
944
|
+
});
|
|
945
|
+
if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
|
|
946
|
+
return readFunctionSseStream(resp);
|
|
947
|
+
});
|
|
948
|
+
setState({
|
|
949
|
+
data: result.value,
|
|
950
|
+
isLoading: false,
|
|
951
|
+
error: null,
|
|
952
|
+
logs: result.logs
|
|
953
|
+
});
|
|
954
|
+
return result.value;
|
|
955
|
+
} catch (err) {
|
|
956
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
957
|
+
const logs = e.logs ?? [];
|
|
958
|
+
setState((s) => ({
|
|
959
|
+
...s,
|
|
960
|
+
isLoading: false,
|
|
961
|
+
error: e,
|
|
962
|
+
logs
|
|
963
|
+
}));
|
|
964
|
+
throw e;
|
|
965
|
+
}
|
|
966
|
+
}, [
|
|
967
|
+
resolved,
|
|
968
|
+
fetcher,
|
|
969
|
+
name
|
|
970
|
+
]),
|
|
971
|
+
data: state.data,
|
|
972
|
+
isLoading: state.isLoading,
|
|
973
|
+
error: state.error,
|
|
974
|
+
logs: state.logs
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
595
978
|
* Run a semantic-layer query against the project's `.view.yml` /
|
|
596
979
|
* `.topic.yml` definitions. The server compiles to SQL and executes
|
|
597
980
|
* through the same connector path as `useQuery`, so result shape
|
|
@@ -639,7 +1022,8 @@ function useSemanticQuery(input, opts = {}) {
|
|
|
639
1022
|
filters: input.filters ?? [],
|
|
640
1023
|
...input.limit != null ? { limit: input.limit } : {}
|
|
641
1024
|
});
|
|
642
|
-
|
|
1025
|
+
const url = `/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`;
|
|
1026
|
+
fetcher(url, {
|
|
643
1027
|
method: "POST",
|
|
644
1028
|
headers: { "content-type": "application/json" },
|
|
645
1029
|
body,
|
|
@@ -649,8 +1033,9 @@ function useSemanticQuery(input, opts = {}) {
|
|
|
649
1033
|
return resp.json();
|
|
650
1034
|
}).then(({ columns, rows, truncated, sql }) => {
|
|
651
1035
|
if (cancelled) return;
|
|
1036
|
+
const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
|
|
652
1037
|
setState({
|
|
653
|
-
rows:
|
|
1038
|
+
rows: objects,
|
|
654
1039
|
columns,
|
|
655
1040
|
truncated,
|
|
656
1041
|
sql: sql ?? null,
|
|
@@ -1037,7 +1422,7 @@ function parseSseData(raw) {
|
|
|
1037
1422
|
* uniformly. New types added upstream don't surface as artifacts
|
|
1038
1423
|
* until added here — that's intentional, the renderer needs to
|
|
1039
1424
|
* know how to display each. */
|
|
1040
|
-
const SQL_EVENT_TYPES = new Set([
|
|
1425
|
+
const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1041
1426
|
"query_executed",
|
|
1042
1427
|
"query_generated",
|
|
1043
1428
|
"verified_sql",
|
|
@@ -1166,6 +1551,82 @@ function sleep(ms, signal) {
|
|
|
1166
1551
|
});
|
|
1167
1552
|
}
|
|
1168
1553
|
/**
|
|
1554
|
+
* Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
|
|
1555
|
+
* `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
|
|
1556
|
+
* (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
|
|
1557
|
+
* grouped by name, with drill-down into recent occurrences.
|
|
1558
|
+
*
|
|
1559
|
+
* The handler returned by [`useTrackEvent`] is **fire-and-forget**:
|
|
1560
|
+
* it enqueues the event into an in-memory batch flushed every second
|
|
1561
|
+
* (and on `pagehide` so a navigation away doesn't drop the tail).
|
|
1562
|
+
* No await semantics — call it inline from a click handler without
|
|
1563
|
+
* awaiting it. Server-side validation errors are logged to the
|
|
1564
|
+
* console; the call site doesn't need to handle them.
|
|
1565
|
+
*
|
|
1566
|
+
* Example:
|
|
1567
|
+
* ```tsx
|
|
1568
|
+
* const track = useTrackEvent();
|
|
1569
|
+
* <button
|
|
1570
|
+
* onClick={() => {
|
|
1571
|
+
* track("export-clicked", { format: "csv", rowCount });
|
|
1572
|
+
* doExport();
|
|
1573
|
+
* }}
|
|
1574
|
+
* >Export</button>
|
|
1575
|
+
* ```
|
|
1576
|
+
*
|
|
1577
|
+
* Rate-limited at 60/min per (user, app) on the server. A burst that
|
|
1578
|
+
* trips the limit drops the excess events with a console warning;
|
|
1579
|
+
* within-limit events are unaffected.
|
|
1580
|
+
*/
|
|
1581
|
+
function useTrackEvent() {
|
|
1582
|
+
const { projectId, fetcher } = useOxyApp();
|
|
1583
|
+
const queueRef = react.useRef([]);
|
|
1584
|
+
const flushTimerRef = react.useRef(null);
|
|
1585
|
+
const flush = react.useCallback(() => {
|
|
1586
|
+
flushTimerRef.current = null;
|
|
1587
|
+
if (!projectId) return;
|
|
1588
|
+
const batch = queueRef.current;
|
|
1589
|
+
if (batch.length === 0) return;
|
|
1590
|
+
queueRef.current = [];
|
|
1591
|
+
for (const evt of batch) {
|
|
1592
|
+
const url = `/api/customer-apps/${projectId}/events`;
|
|
1593
|
+
const body = JSON.stringify(evt);
|
|
1594
|
+
try {
|
|
1595
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
1596
|
+
else fetcher(url, {
|
|
1597
|
+
method: "POST",
|
|
1598
|
+
headers: { "content-type": "application/json" },
|
|
1599
|
+
body
|
|
1600
|
+
}).catch((e) => {
|
|
1601
|
+
console.warn("[oxy] useTrackEvent flush failed:", e);
|
|
1602
|
+
});
|
|
1603
|
+
} catch (e) {
|
|
1604
|
+
console.warn("[oxy] useTrackEvent enqueue failed:", e);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}, [projectId, fetcher]);
|
|
1608
|
+
react.useEffect(() => {
|
|
1609
|
+
if (typeof window === "undefined") return;
|
|
1610
|
+
const onHide = () => flush();
|
|
1611
|
+
window.addEventListener("pagehide", onHide);
|
|
1612
|
+
return () => {
|
|
1613
|
+
window.removeEventListener("pagehide", onHide);
|
|
1614
|
+
flush();
|
|
1615
|
+
if (flushTimerRef.current !== null) {
|
|
1616
|
+
clearTimeout(flushTimerRef.current);
|
|
1617
|
+
flushTimerRef.current = null;
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
}, [flush]);
|
|
1621
|
+
return react.useCallback((name, payload) => {
|
|
1622
|
+
queueRef.current.push({
|
|
1623
|
+
event_name: name,
|
|
1624
|
+
payload: payload ?? {}
|
|
1625
|
+
});
|
|
1626
|
+
if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
|
|
1627
|
+
}, [flush]);
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1169
1630
|
* Renders an agent run's answer + artifacts + thread link as a
|
|
1170
1631
|
* single block. The default styling is intentionally neutral
|
|
1171
1632
|
* (system fonts, gray surfaces) so it blends into any bundle.
|
|
@@ -1187,25 +1648,55 @@ function OxyAnswer(props) {
|
|
|
1187
1648
|
const isRunning = state === "running";
|
|
1188
1649
|
const isFailed = state === "failed";
|
|
1189
1650
|
const needsClarification = state === "needs_clarification";
|
|
1190
|
-
return /* @__PURE__ */
|
|
1651
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1191
1652
|
className,
|
|
1192
|
-
style: styles.answerWrap
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1653
|
+
style: styles.answerWrap,
|
|
1654
|
+
children: [
|
|
1655
|
+
isRunning && answer === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1656
|
+
style: styles.statusRow,
|
|
1657
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1658
|
+
style: styles.spinner,
|
|
1659
|
+
"aria-hidden": "true"
|
|
1660
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1661
|
+
style: styles.statusText,
|
|
1662
|
+
children: "Thinking…"
|
|
1663
|
+
})]
|
|
1664
|
+
}) : null,
|
|
1665
|
+
artifacts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1666
|
+
style: styles.artifactList,
|
|
1667
|
+
children: artifacts.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SqlArtifactBlock, {
|
|
1668
|
+
artifact: a,
|
|
1669
|
+
maxRows: maxArtifactRows
|
|
1670
|
+
}, a.id))
|
|
1671
|
+
}) : null,
|
|
1672
|
+
answer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1673
|
+
style: styles.markdown,
|
|
1674
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkdownText, { text: answer })
|
|
1675
|
+
}) : null,
|
|
1676
|
+
needsClarification && clarification ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1677
|
+
style: styles.clarification,
|
|
1678
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1679
|
+
style: { marginTop: 4 },
|
|
1680
|
+
children: clarification
|
|
1681
|
+
})]
|
|
1682
|
+
}) : null,
|
|
1683
|
+
isFailed && error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBlock, { error }) : null,
|
|
1684
|
+
threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1685
|
+
style: styles.threadLinkRow,
|
|
1686
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
1687
|
+
href: threadUrl,
|
|
1688
|
+
target: "_blank",
|
|
1689
|
+
rel: "noreferrer noopener",
|
|
1690
|
+
style: styles.threadLink,
|
|
1691
|
+
children: [threadLinkLabel, " →"]
|
|
1692
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1693
|
+
style: styles.betaBadge,
|
|
1694
|
+
title: "Thread linking is in beta — see docs",
|
|
1695
|
+
children: "beta"
|
|
1696
|
+
})]
|
|
1697
|
+
}) : null
|
|
1698
|
+
]
|
|
1699
|
+
});
|
|
1209
1700
|
}
|
|
1210
1701
|
/**
|
|
1211
1702
|
* Complete drop-in chat surface. One agent, one input, one answer
|
|
@@ -1229,37 +1720,48 @@ function OxyChat(props) {
|
|
|
1229
1720
|
if (!q || run.state === "running") return;
|
|
1230
1721
|
run.ask(q);
|
|
1231
1722
|
}, [question, run]);
|
|
1232
|
-
return /* @__PURE__ */
|
|
1723
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1233
1724
|
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
|
-
|
|
1725
|
+
style: styles.chatWrap,
|
|
1726
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1727
|
+
onSubmit: submit,
|
|
1728
|
+
style: styles.chatForm,
|
|
1729
|
+
children: [
|
|
1730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1731
|
+
type: "text",
|
|
1732
|
+
value: question,
|
|
1733
|
+
onChange: (e) => setQuestion(e.target.value),
|
|
1734
|
+
placeholder,
|
|
1735
|
+
disabled: run.state === "running",
|
|
1736
|
+
style: styles.chatInput,
|
|
1737
|
+
"aria-label": "Question"
|
|
1738
|
+
}),
|
|
1739
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1740
|
+
type: "submit",
|
|
1741
|
+
disabled: run.state === "running" || question.trim() === "",
|
|
1742
|
+
style: styles.chatSubmit,
|
|
1743
|
+
children: run.state === "running" ? "…" : submitLabel
|
|
1744
|
+
}),
|
|
1745
|
+
run.state === "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1746
|
+
type: "button",
|
|
1747
|
+
onClick: run.cancel,
|
|
1748
|
+
style: styles.chatCancel,
|
|
1749
|
+
children: "Stop"
|
|
1750
|
+
}) : null
|
|
1751
|
+
]
|
|
1752
|
+
}), run.state === "idle" ? emptyState ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1753
|
+
style: styles.emptyState,
|
|
1754
|
+
children: "Ask a question to get started."
|
|
1755
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAnswer, {
|
|
1756
|
+
answer: run.answer,
|
|
1757
|
+
artifacts: run.artifacts,
|
|
1758
|
+
state: run.state,
|
|
1759
|
+
clarification: run.clarification,
|
|
1760
|
+
error: run.error,
|
|
1761
|
+
threadUrl: run.threadUrl,
|
|
1762
|
+
maxArtifactRows
|
|
1763
|
+
})]
|
|
1764
|
+
});
|
|
1263
1765
|
}
|
|
1264
1766
|
function SqlArtifactBlock(props) {
|
|
1265
1767
|
const { artifact, maxRows } = props;
|
|
@@ -1268,17 +1770,53 @@ function SqlArtifactBlock(props) {
|
|
|
1268
1770
|
const truncated = results ? results.rows.length > maxRows : false;
|
|
1269
1771
|
const visibleRows = results ? results.rows.slice(0, maxRows) : [];
|
|
1270
1772
|
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
|
-
|
|
1773
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1774
|
+
style: styles.artifact,
|
|
1775
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1776
|
+
type: "button",
|
|
1777
|
+
onClick: () => setOpen((o) => !o),
|
|
1778
|
+
style: styles.artifactHeader,
|
|
1779
|
+
children: [
|
|
1780
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1781
|
+
style: styles.artifactBadge,
|
|
1782
|
+
children: sourceLabel
|
|
1783
|
+
}),
|
|
1784
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1785
|
+
style: styles.artifactSummary,
|
|
1786
|
+
children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
|
|
1787
|
+
}),
|
|
1788
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1789
|
+
style: styles.artifactToggle,
|
|
1790
|
+
children: open ? "Hide" : "Show"
|
|
1791
|
+
})
|
|
1792
|
+
]
|
|
1793
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1794
|
+
style: styles.sqlBlock,
|
|
1795
|
+
children: artifact.sql
|
|
1796
|
+
}), artifact.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1797
|
+
style: styles.error,
|
|
1798
|
+
children: artifact.error
|
|
1799
|
+
}) : results ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1800
|
+
style: styles.resultsWrap,
|
|
1801
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
1802
|
+
style: styles.resultsTable,
|
|
1803
|
+
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", {
|
|
1804
|
+
style: styles.resultsTh,
|
|
1805
|
+
children: c
|
|
1806
|
+
}, 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", {
|
|
1807
|
+
style: styles.resultsTd,
|
|
1808
|
+
children: formatCell(cell)
|
|
1809
|
+
}, j)) }, i)) })]
|
|
1810
|
+
}), truncated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1811
|
+
style: styles.truncatedNote,
|
|
1812
|
+
children: [
|
|
1813
|
+
"+",
|
|
1814
|
+
results.rows.length - maxRows,
|
|
1815
|
+
" more rows. Open the thread in Oxy to see all."
|
|
1816
|
+
]
|
|
1817
|
+
}) : null]
|
|
1818
|
+
}) : null] }) : null]
|
|
1819
|
+
});
|
|
1282
1820
|
}
|
|
1283
1821
|
function formatCell(value) {
|
|
1284
1822
|
if (value === null || value === void 0) return "—";
|
|
@@ -1293,16 +1831,36 @@ function formatCell(value) {
|
|
|
1293
1831
|
*/
|
|
1294
1832
|
function ErrorBlock(props) {
|
|
1295
1833
|
const { error } = props;
|
|
1296
|
-
if (error instanceof OxyApiError) return /* @__PURE__ */
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1834
|
+
if (error instanceof OxyApiError) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1835
|
+
style: styles.error,
|
|
1836
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
1837
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
|
|
1838
|
+
" ",
|
|
1839
|
+
error.message.split("\n\n")[0]
|
|
1840
|
+
] }), error.hint ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1841
|
+
style: {
|
|
1842
|
+
marginTop: 6,
|
|
1843
|
+
fontWeight: 400,
|
|
1844
|
+
whiteSpace: "pre-wrap"
|
|
1845
|
+
},
|
|
1846
|
+
children: [
|
|
1847
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Hint:" }),
|
|
1848
|
+
" ",
|
|
1849
|
+
error.hint
|
|
1850
|
+
]
|
|
1851
|
+
}) : null]
|
|
1852
|
+
});
|
|
1853
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1854
|
+
style: styles.error,
|
|
1855
|
+
children: [
|
|
1856
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
|
|
1857
|
+
" ",
|
|
1858
|
+
error.message
|
|
1859
|
+
]
|
|
1860
|
+
});
|
|
1302
1861
|
}
|
|
1303
1862
|
function MarkdownText(props) {
|
|
1304
|
-
|
|
1305
|
-
return /* @__PURE__ */ react.createElement(react.Fragment, null, blocks);
|
|
1863
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: react.useMemo(() => parseMarkdown(props.text), [props.text]) });
|
|
1306
1864
|
}
|
|
1307
1865
|
function parseMarkdown(text) {
|
|
1308
1866
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
@@ -1372,27 +1930,23 @@ function parseMarkdown(text) {
|
|
|
1372
1930
|
}
|
|
1373
1931
|
return blocks.map((b, idx) => {
|
|
1374
1932
|
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,
|
|
1933
|
+
case "h": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(`h${b.level}`, {
|
|
1934
|
+
style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
|
|
1935
|
+
children: renderInline(b.text)
|
|
1936
|
+
}, idx);
|
|
1937
|
+
case "code": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1385
1938
|
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
|
-
|
|
1939
|
+
"data-lang": b.lang || void 0,
|
|
1940
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: b.code })
|
|
1941
|
+
}, idx);
|
|
1942
|
+
case "list": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1943
|
+
style: styles.list,
|
|
1944
|
+
children: b.items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: renderInline(item) }, i))
|
|
1945
|
+
}, idx);
|
|
1946
|
+
case "p": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1947
|
+
style: styles.paragraph,
|
|
1948
|
+
children: renderInline(b.text)
|
|
1949
|
+
}, idx);
|
|
1396
1950
|
}
|
|
1397
1951
|
});
|
|
1398
1952
|
}
|
|
@@ -1408,27 +1962,31 @@ function renderInline(text) {
|
|
|
1408
1962
|
const patterns = [
|
|
1409
1963
|
{
|
|
1410
1964
|
re: /`([^`]+)`/,
|
|
1411
|
-
render: (m) => /* @__PURE__ */
|
|
1965
|
+
render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1966
|
+
style: styles.inlineCode,
|
|
1967
|
+
children: m[1]
|
|
1968
|
+
})
|
|
1412
1969
|
},
|
|
1413
1970
|
{
|
|
1414
1971
|
re: /\[([^\]]+)\]\(([^)]+)\)/,
|
|
1415
1972
|
render: (m) => {
|
|
1416
|
-
if (isSafeLinkHref(m[2])) return /* @__PURE__ */
|
|
1973
|
+
if (isSafeLinkHref(m[2])) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1417
1974
|
href: m[2],
|
|
1418
1975
|
target: "_blank",
|
|
1419
1976
|
rel: "noreferrer noopener",
|
|
1420
|
-
style: styles.link
|
|
1421
|
-
|
|
1422
|
-
|
|
1977
|
+
style: styles.link,
|
|
1978
|
+
children: m[1]
|
|
1979
|
+
});
|
|
1980
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: m[1] });
|
|
1423
1981
|
}
|
|
1424
1982
|
},
|
|
1425
1983
|
{
|
|
1426
1984
|
re: /\*\*([^*]+)\*\*/,
|
|
1427
|
-
render: (m) => /* @__PURE__ */
|
|
1985
|
+
render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: m[1] })
|
|
1428
1986
|
},
|
|
1429
1987
|
{
|
|
1430
1988
|
re: /\*([^*]+)\*/,
|
|
1431
|
-
render: (m) => /* @__PURE__ */
|
|
1989
|
+
render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: m[1] })
|
|
1432
1990
|
}
|
|
1433
1991
|
];
|
|
1434
1992
|
while (remaining.length > 0) {
|
|
@@ -1446,7 +2004,7 @@ function renderInline(text) {
|
|
|
1446
2004
|
break;
|
|
1447
2005
|
}
|
|
1448
2006
|
if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
|
|
1449
|
-
segments.push(/* @__PURE__ */
|
|
2007
|
+
segments.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: earliest.node }, key++));
|
|
1450
2008
|
remaining = remaining.slice(earliest.idx + earliest.len);
|
|
1451
2009
|
}
|
|
1452
2010
|
return segments;
|
|
@@ -1676,11 +2234,142 @@ const styles = {
|
|
|
1676
2234
|
};
|
|
1677
2235
|
|
|
1678
2236
|
//#endregion
|
|
2237
|
+
//#region src/metricTree.ts
|
|
2238
|
+
/**
|
|
2239
|
+
* Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
|
|
2240
|
+
* airlayer metric-tree analyses (tree introspection, sensitivity, predict,
|
|
2241
|
+
* explain, opportunity) over typed methods.
|
|
2242
|
+
*
|
|
2243
|
+
* Construction is internal to {@link OxyClient} — call `client.metricTree`
|
|
2244
|
+
* to access an instance rather than building one yourself.
|
|
2245
|
+
*
|
|
2246
|
+
* @example
|
|
2247
|
+
* ```typescript
|
|
2248
|
+
* const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
|
|
2249
|
+
* const tree = await client.metricTree.getTree();
|
|
2250
|
+
* const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2251
|
+
* ```
|
|
2252
|
+
*/
|
|
2253
|
+
var MetricTreeClient = class {
|
|
2254
|
+
constructor(config, request) {
|
|
2255
|
+
this.config = config;
|
|
2256
|
+
this.request = request;
|
|
2257
|
+
}
|
|
2258
|
+
path(suffix) {
|
|
2259
|
+
return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
|
|
2260
|
+
}
|
|
2261
|
+
buildQuery(extra = {}) {
|
|
2262
|
+
const params = { ...extra };
|
|
2263
|
+
if (this.config.branch) params.branch = this.config.branch;
|
|
2264
|
+
const qs = new URLSearchParams(params).toString();
|
|
2265
|
+
return qs ? `?${qs}` : "";
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Fetch the full metric tree, or the subtree rooted at `root`.
|
|
2269
|
+
*
|
|
2270
|
+
* @param root - Optional fully-qualified measure id to root the tree at.
|
|
2271
|
+
* @returns Nodes (measures) and edges (component / driver relationships).
|
|
2272
|
+
*
|
|
2273
|
+
* @example
|
|
2274
|
+
* ```typescript
|
|
2275
|
+
* const tree = await client.metricTree.getTree();
|
|
2276
|
+
* const subtree = await client.metricTree.getTree("orders.net_revenue");
|
|
2277
|
+
* ```
|
|
2278
|
+
*/
|
|
2279
|
+
async getTree(root) {
|
|
2280
|
+
const query = this.buildQuery(root ? { root } : {});
|
|
2281
|
+
return this.request(this.path(query));
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Rank the declared drivers of a measure by influence.
|
|
2285
|
+
*
|
|
2286
|
+
* @param measureId - Fully-qualified measure id (`view.measure`).
|
|
2287
|
+
*
|
|
2288
|
+
* @example
|
|
2289
|
+
* ```typescript
|
|
2290
|
+
* const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
|
|
2291
|
+
* for (const driver of sensitivity.drivers) {
|
|
2292
|
+
* console.log(driver.measure, driver.direction, driver.strength);
|
|
2293
|
+
* }
|
|
2294
|
+
* ```
|
|
2295
|
+
*/
|
|
2296
|
+
async getSensitivity(measureId) {
|
|
2297
|
+
const query = this.buildQuery();
|
|
2298
|
+
return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Propagate hypothetical `(measure, delta)` changes upward through the
|
|
2302
|
+
* tree. Returns the estimated impact on every downstream measure.
|
|
2303
|
+
*
|
|
2304
|
+
* @example
|
|
2305
|
+
* ```typescript
|
|
2306
|
+
* const result = await client.metricTree.predict([
|
|
2307
|
+
* { measure: "marketing_spend.total_spend", delta: 10000 },
|
|
2308
|
+
* ]);
|
|
2309
|
+
* ```
|
|
2310
|
+
*/
|
|
2311
|
+
async predict(changes) {
|
|
2312
|
+
const query = this.buildQuery();
|
|
2313
|
+
return this.request(this.path(`/predict${query}`), {
|
|
2314
|
+
method: "POST",
|
|
2315
|
+
body: JSON.stringify({ changes })
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Period-over-period root-cause decomposition. Recursively splits the
|
|
2320
|
+
* target measure by components and dimensions until the move concentrates.
|
|
2321
|
+
*
|
|
2322
|
+
* @example
|
|
2323
|
+
* ```typescript
|
|
2324
|
+
* const result = await client.metricTree.explain({
|
|
2325
|
+
* target: "financials.operating_profit",
|
|
2326
|
+
* time_dimension: "financials.month",
|
|
2327
|
+
* current_period: ["2025-09-01", "2025-09-30"],
|
|
2328
|
+
* previous_period: ["2025-08-01", "2025-08-31"],
|
|
2329
|
+
* });
|
|
2330
|
+
* ```
|
|
2331
|
+
*/
|
|
2332
|
+
async explain(request) {
|
|
2333
|
+
const query = this.buildQuery();
|
|
2334
|
+
return this.request(this.path(`/explain${query}`), {
|
|
2335
|
+
method: "POST",
|
|
2336
|
+
body: JSON.stringify(request)
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Size the upside opportunity for a measure by finding underperforming
|
|
2341
|
+
* segments. Skips high-cardinality dimensions and trims the long tail.
|
|
2342
|
+
*
|
|
2343
|
+
* @example
|
|
2344
|
+
* ```typescript
|
|
2345
|
+
* const result = await client.metricTree.findOpportunities({
|
|
2346
|
+
* target: "orders.net_revenue",
|
|
2347
|
+
* time_dimension: "orders.order_date",
|
|
2348
|
+
* period: ["2025-09-01", "2025-09-30"],
|
|
2349
|
+
* });
|
|
2350
|
+
* for (const dim of result.dimensions) {
|
|
2351
|
+
* console.log(dim.dimension, "+", dim.total_upside);
|
|
2352
|
+
* }
|
|
2353
|
+
* ```
|
|
2354
|
+
*/
|
|
2355
|
+
async findOpportunities(request) {
|
|
2356
|
+
const query = this.buildQuery();
|
|
2357
|
+
return this.request(this.path(`/opportunity${query}`), {
|
|
2358
|
+
method: "POST",
|
|
2359
|
+
body: JSON.stringify(request)
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
//#endregion
|
|
2365
|
+
exports.AnomaliesClient = AnomaliesClient;
|
|
2366
|
+
exports.MetricTreeClient = MetricTreeClient;
|
|
1679
2367
|
exports.OxyAnswer = OxyAnswer;
|
|
1680
2368
|
exports.OxyApiError = OxyApiError;
|
|
1681
2369
|
exports.OxyAppProvider = OxyAppProvider;
|
|
1682
2370
|
exports.OxyChat = OxyChat;
|
|
1683
2371
|
exports._resetCustomerAppManifestCacheForTest = _resetCustomerAppManifestCacheForTest;
|
|
2372
|
+
exports.apiErrorFromResponse = apiErrorFromResponse;
|
|
1684
2373
|
exports.getCustomerAppDebug = getCustomerAppDebug;
|
|
1685
2374
|
exports.getOxyAppLogger = getOxyAppLogger;
|
|
1686
2375
|
exports.interpretCustomerAppError = interpretCustomerAppError;
|
|
@@ -1688,8 +2377,10 @@ exports.loadCustomerAppManifest = loadCustomerAppManifest;
|
|
|
1688
2377
|
exports.readInjectedAppConfig = readInjectedAppConfig;
|
|
1689
2378
|
exports.setOxyAppLogger = setOxyAppLogger;
|
|
1690
2379
|
exports.useAgentRun = useAgentRun;
|
|
2380
|
+
exports.useFunction = useFunction;
|
|
1691
2381
|
exports.useProcedureRun = useProcedureRun;
|
|
1692
2382
|
exports.useQuery = useQuery;
|
|
1693
2383
|
exports.useResolvedManifest = useResolvedManifest;
|
|
1694
2384
|
exports.useSemanticQuery = useSemanticQuery;
|
|
2385
|
+
exports.useTrackEvent = useTrackEvent;
|
|
1695
2386
|
//# sourceMappingURL=index.cjs.map
|