@uncoil.dev/sdk 0.0.0-stage → 0.6.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 CHANGED
@@ -1,3 +1,217 @@
1
- # Temporary Holding Version
1
+ # Uncoil TypeScript SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Observability and tracing for AI agents. Gain full visibility into your agent's decision-making process, tool usage, and LLM costs.
4
+
5
+ Uncoil records each agent run as one trace: every model call, tool call, retry, failure and cost. It also ingests OpenTelemetry. Free plan available. Docs: [uncoil.dev/docs](https://www.uncoil.dev/docs). New to agent observability? Read the [complete guide](https://www.uncoil.dev/blog/ai-agent-observability-complete-guide).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @uncoil.dev/sdk
11
+ # or
12
+ yarn add @uncoil.dev/sdk
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ 1. Get your API Key from the [Uncoil Dashboard](https://www.uncoil.dev).
18
+ 2. Set it as an environment variable:
19
+
20
+ ```bash
21
+ export UNCOIL_API_KEY=your_sk_live_...
22
+ ```
23
+
24
+ 3. Instrument your agent:
25
+
26
+ ```typescript
27
+ import { traceAgent, llmCall, toolCall } from '@uncoil.dev/sdk';
28
+
29
+ const agent = traceAgent(async (query: string) => {
30
+ // LLM Call
31
+ await llmCall({
32
+ input: query,
33
+ output: "Thinking...",
34
+ modelId: "gpt-4o",
35
+ costUsd: 0.002,
36
+ latencyMs: 800
37
+ });
38
+
39
+ // Tool Use
40
+ await toolCall({
41
+ toolName: "web_search",
42
+ input: { q: query },
43
+ output: "Found results...",
44
+ latencyMs: 1200
45
+ });
46
+
47
+ return "Final Answer";
48
+ }, { environment: "development", release: "quickstart" });
49
+ ```
50
+
51
+ ## Steps
52
+
53
+ Inside a `traceAgent` function, every step you record is linked to the step it ran in, so the trace graph
54
+ shows which step called which and how loops repeat. Use `span` for a block and `observe` for a function:
55
+
56
+ ```ts
57
+ import { observe, span, traceAgent } from "@uncoil.dev/sdk";
58
+
59
+ const searchDocs = observe(async function searchDocs(query: string) {
60
+ return search(query);
61
+ }, { type: "tool" });
62
+
63
+ export const answer = traceAgent(async (question: string) => {
64
+ for (let turn = 0; turn < 5; turn++) {
65
+ const reply = await span("think", () =>
66
+ span("chat", async (step) => {
67
+ const reply = await callModel(question);
68
+ step.set({ model: "claude-sonnet-5-5", inputTokens: reply.usage.input, outputTokens: reply.usage.output });
69
+ return reply;
70
+ }, { type: "llm", input: question }),
71
+ { type: "agent" });
72
+ if (reply.done) return reply.text;
73
+ await searchDocs(reply.query);
74
+ }
75
+ });
76
+ ```
77
+
78
+ `type` is one of `agent`, `llm`, `tool`, `retrieval` or `span`. Errors are recorded with their type and
79
+ message. Concurrent runs stay in separate traces (Node's AsyncLocalStorage). Inputs and outputs have obvious
80
+ secrets (bearer tokens, API keys, JWTs, `password=` values) removed and are capped at 10,000 characters;
81
+ avoid sending anything else you would not want stored.
82
+
83
+ ## Features
84
+
85
+ - **traceAgent Wrapper:** Automatically captures run lifecycle and errors.
86
+ - **Granular Spans:** Manually record LLMs, tools, and custom decisions.
87
+ - **Environment Aware:** Automatically picks up API keys from `process.env`.
88
+ - **Lightweight:** Minimal dependencies, optimized for Node.js and Edge runtimes.
89
+ - **Session context:** Add `sessionId`, `endUserId`, `environment`, `release`, and `tags` to connect related traces.
90
+
91
+ ## Feedback and evaluation scores
92
+
93
+ Pass `projectId` when constructing the client to attach end-user feedback and typed scores to traces:
94
+
95
+ ```typescript
96
+ const client = new UncoilClient({ apiKey: process.env.UNCOIL_API_KEY, projectId: "your-project-id" });
97
+ client.feedback(runId, true, { kind: "thumb", comment: "Helpful answer" });
98
+ client.score(runId, "answer_quality", 0.92, { dataType: "numeric" });
99
+ ```
100
+
101
+ These helpers use the project API key, deduplicate when given a `dedupeKey`, and never throw into the agent process.
102
+
103
+ ## Prompt deployment
104
+
105
+ Resolve a deployed prompt without shipping a new application build:
106
+
107
+ ```typescript
108
+ const deployed = await client.getPrompt("support-agent", "production");
109
+ await client.ingest({ spanType: "llm", input: question, output, promptVersionId: deployed.version.id });
110
+ ```
111
+
112
+ Prompt responses are cached for 60 seconds by default. Pass `{ cacheTtlMs: 0, fallback }` for development or guaranteed-availability behavior.
113
+
114
+ ## License
115
+
116
+ MIT
117
+
118
+ ## Provider integrations (0.4.0)
119
+
120
+ Wrap an existing client once. Its normal calls, typed results, streaming chunks and errors keep working.
121
+ Provider packages are optional; install the providers you use. Node.js is required (async context propagation).
122
+
123
+ ```ts
124
+ import OpenAI from "openai";
125
+ import Anthropic from "@anthropic-ai/sdk";
126
+ import { wrapOpenAI, wrapAnthropic, UncoilLangGraphHandler, traceAgent } from "@uncoil.dev/sdk";
127
+
128
+ const openai = wrapOpenAI(new OpenAI());
129
+ const anthropic = wrapAnthropic(new Anthropic());
130
+ // Existing calls now record model, input/output, latency, tokens, TTFT and provider errors.
131
+ const reply = await openai.responses.create({ model: process.env.OPENAI_MODEL!, input: "Hello" });
132
+ const stream = await anthropic.messages.create({
133
+ model: process.env.ANTHROPIC_MODEL!, max_tokens: 100,
134
+ messages: [{ role: "user", content: "Hello" }], stream: true,
135
+ });
136
+ for await (const event of stream) { /* existing handling */ }
137
+
138
+ // For LangGraph, pass one reusable handler in the graph invocation config.
139
+ await graph.invoke(input, { callbacks: [new UncoilLangGraphHandler()] });
140
+ ```
141
+
142
+ Use `UNCOIL_API_KEY` and the existing region/host configuration. Pass `{ client }` for a custom
143
+ `UncoilClient`. Calls inside `traceAgent`/`span` inherit the current run and parent; standalone provider
144
+ calls get a completed root, so unrelated requests never share an `unknown` run. LangGraph callbacks
145
+ record graph/node/tool/model parent relationships. Use either framework model callbacks or a wrapped
146
+ provider for each model call to avoid recording that call twice.
147
+
148
+ Supported APIs: OpenAI `chat.completions.create`, `responses.create`, and their `.stream()` helpers;
149
+ Anthropic `messages.create`, `messages.stream`, and beta messages equivalents. Async iteration and
150
+ helper event/final-result APIs are observed. OpenAI `.withResponse()` retains headers/request IDs;
151
+ `.asResponse()` remains unparsed and records request timing only. Stream conversion helpers such as
152
+ `.tee()` and `.toReadableStream()` are passed through and do not observe consumption: iterate the
153
+ wrapped stream for complete usage/TTFT. Explicit `stream_options.include_usage: false` is respected.
154
+ Cancel/abort records partial output once; telemetry failure never replaces the provider's result or error.
155
+
156
+ Payloads use the SDK's existing redaction and size caps. Disable capture with
157
+ `{ captureInput: false, captureOutput: false }`. Provider usage is stored exactly as returned, including
158
+ cache breakdown in metadata. **Prices are not guessed:** cost defaults to zero with
159
+ `metadata.costKnown=false`. Supply `calculateCost({provider, model, inputTokens, outputTokens, usage})`
160
+ using your own current rate card to record USD cost. Missing provider usage remains unknown, not an
161
+ estimated token count. None of these wrappers publishes your key or changes provider billing.
162
+
163
+ Anthropic total input tokens include uncached input plus cache reads and cache writes; the original
164
+ usage breakdown remains in metadata for rate-card calculations. See the [provider’s token breakdown](https://platform.claude.com/docs/en/build-with-claude/prompt-caching).
165
+
166
+ ## Framework one-liners
167
+
168
+ Quickest start: call `init()` once at startup and every OpenAI and Anthropic call is traced, with no client wrapping (`UNCOIL_API_KEY` is read from the environment).
169
+
170
+ ```ts
171
+ import { init } from "@uncoil.dev/sdk";
172
+ init();
173
+ ```
174
+
175
+ `init()` patches the installed `openai` and `@anthropic-ai/sdk` (both the CommonJS and ES-module builds; `await init().ready` if your first call happens instantly). Call the function it returns to switch it off. Use it or the per-framework options below for the same calls, not both. LangChain / LangGraph need `callbacks: [new UncoilLangGraphHandler()]`, and the Vercel AI SDK needs the middleware shown here.
176
+
177
+ ```ts
178
+ import { wrapLanguageModel } from "ai";
179
+ import { uncoilMiddleware, traceTools } from "@uncoil.dev/sdk";
180
+
181
+ const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: uncoilMiddleware() }); // Vercel AI SDK
182
+ const tools = traceTools({ searchDocs, sendEmail }); // any functions or { execute } tool objects
183
+ ```
184
+
185
+ OpenAI (`wrapOpenAI`), Anthropic (`wrapAnthropic`) and LangGraph (`UncoilLangGraphHandler`) work as before. Copy-paste starters live in [`templates/`](./templates).
186
+
187
+ ## Sampling, redaction and clearer errors
188
+
189
+ ```ts
190
+ const uncoil = new UncoilClient({
191
+ sampleRate: 0.1, // record about 1 in 10 runs; each run is kept or dropped whole
192
+ redact: { patterns: [/ACME-\d{6}/] }, // emails, phones and card numbers are masked by default; add your own patterns
193
+ onError: (error) => log(error.message), // problems come with a plain-language fix (error.hint)
194
+ });
195
+ ```
196
+
197
+ Spans that report an error are kept even when their run was not sampled (`keepErrors: false` turns that off). Secrets such as bearer tokens and API keys are always removed, and emails, phone numbers and card numbers are masked before anything leaves your app. To send them as they are, set `redact: { pii: false }`.
198
+
199
+ ### Sign in from the terminal
200
+
201
+ ```bash
202
+ npx @uncoil.dev/sdk login
203
+ ```
204
+
205
+ Opens your browser so you can sign in (or create a free account), pick or create a project, and confirm the code shown in your terminal. The key is then saved to `.env.local` as `UNCOIL_API_KEY`, with `UNCOIL_PROJECT_ID` next to it (and `UNCOIL_REGION` for the US region). In a git repo the file is added to `.gitignore` if it is not already ignored. The key travels encrypted to your machine and is never printed. Options: `--region eu|us`, `--new <name>` (create a project with this name), `--no-browser` (only print the address and code), `--env-file <path>` and `--json`. Picking a project that already sends data replaces its key, and the browser asks you to confirm that first. Plain Node does not read `.env.local` by itself; run `node --env-file=.env.local app.js` or load it with dotenv.
206
+
207
+ `npx @uncoil.dev/sdk doctor` also reads `.env.local`/`.env`, and now tells you which project traces will land in (`Traces will land in: <name>`). It fails if `UNCOIL_PROJECT_ID` points at a different project than the key.
208
+
209
+ ## Setup checker
210
+
211
+ Not seeing traces? One command checks your key, region and network, then sends a test trace:
212
+
213
+ ```bash
214
+ npx @uncoil.dev/sdk doctor
215
+ ```
216
+
217
+ It reads `UNCOIL_API_KEY` (and `UNCOIL_REGION` / `UNCOIL_HOST` if set), tells you in plain words what is wrong and how to fix it, and exits with code 1 if anything fails. Options: `--key`, `--region eu|us`, `--host`, `--no-send` (skip the test trace) and `--json`. You can also call `runDoctor()` from code. The test trace is named "Uncoil setup check" and tagged `uncoil-doctor`; it counts as one trace on your plan.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const doctor_1 = require("./doctor");
5
+ const login_1 = require("./login");
6
+ const HELP = `Usage: uncoil login [options]
7
+ uncoil doctor [options]
8
+
9
+ login Sign in through your browser and save the API key for this folder.
10
+ doctor Check your API key, region and network, then send one test trace.
11
+
12
+ login options:
13
+ --region <eu|us> Data region (default: UNCOIL_REGION, else eu)
14
+ --new <name> Create a new project with this name
15
+ --no-browser Only print the address and code; do not open a browser
16
+ --env-file <path> Where to save the key (default: .env.local)
17
+ --host <url> Custom host (default: UNCOIL_HOST)
18
+ --json Print machine-readable lines
19
+
20
+ doctor options:
21
+
22
+ --key <key> API key (default: UNCOIL_API_KEY)
23
+ --region <eu|us> Data region (default: UNCOIL_REGION, else from the key)
24
+ --host <url> Custom host (default: UNCOIL_HOST)
25
+ --no-send Skip the test trace
26
+ --json Print the result as JSON
27
+ -h, --help Show this help
28
+
29
+ Exit code is 0 when everything passes, 1 when a check fails.`;
30
+ async function main(argv) {
31
+ const command = argv[0] === "login" || argv[0] === "doctor" ? argv[0] : "doctor";
32
+ const args = argv[0] === "login" || argv[0] === "doctor" ? argv.slice(1) : argv;
33
+ const options = {};
34
+ let json = false;
35
+ let newProject;
36
+ for (let i = 0; i < args.length; i++) {
37
+ const arg = args[i];
38
+ const value = () => {
39
+ const next = args[++i];
40
+ if (!next) {
41
+ console.error(`Missing value for ${arg}.\n\n${HELP}`);
42
+ process.exit(2);
43
+ }
44
+ return next;
45
+ };
46
+ if (arg === "-h" || arg === "--help") {
47
+ console.log(HELP);
48
+ return 0;
49
+ }
50
+ else if (arg === "--key" && command === "doctor")
51
+ options.apiKey = value();
52
+ else if (arg === "--region") {
53
+ const region = value().toLowerCase();
54
+ if (region !== "eu" && region !== "us") {
55
+ console.error('--region must be "eu" or "us".');
56
+ return 2;
57
+ }
58
+ options.region = region;
59
+ }
60
+ else if (arg === "--host")
61
+ options.host = value();
62
+ else if (arg === "--no-send" && command === "doctor")
63
+ options.send = false;
64
+ else if (arg === "--new" && command === "login")
65
+ newProject = value();
66
+ else if (arg === "--no-browser" && command === "login")
67
+ options.browser = false;
68
+ else if (arg === "--env-file" && command === "login")
69
+ options.envFile = value();
70
+ else if (arg === "--json")
71
+ json = true;
72
+ else {
73
+ console.error(`Unknown option ${arg}.\n\n${HELP}`);
74
+ return 2;
75
+ }
76
+ }
77
+ if (command === "login") {
78
+ const result = await (0, login_1.runLogin)({ ...options, newProject, json });
79
+ if (!result.ok)
80
+ console.error(json ? JSON.stringify({ event: "error", message: result.message }) : result.message);
81
+ return result.ok ? 0 : 1;
82
+ }
83
+ (0, login_1.loadUncoilEnv)(process.cwd());
84
+ const report = await (0, doctor_1.runDoctor)(options);
85
+ console.log(json ? JSON.stringify(report, null, 2) : (0, doctor_1.formatReport)(report));
86
+ return report.ok ? 0 : 1;
87
+ }
88
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
@@ -0,0 +1,33 @@
1
+ import { type UncoilRegion } from "./index";
2
+ export type DoctorStatus = "ok" | "warn" | "fail" | "skip";
3
+ export interface DoctorCheck {
4
+ id: "runtime" | "key" | "region" | "network" | "project" | "trace";
5
+ title: string;
6
+ status: DoctorStatus;
7
+ message: string;
8
+ /** Plain-language fix, present when the check did not pass. */
9
+ hint?: string;
10
+ }
11
+ export interface DoctorOptions {
12
+ apiKey?: string;
13
+ region?: UncoilRegion;
14
+ host?: string;
15
+ /** Skip the test trace; only check the key shape, region and network. */
16
+ send?: boolean;
17
+ /** Project the key is expected to belong to. Defaults to UNCOIL_PROJECT_ID. */
18
+ projectId?: string;
19
+ /** Seconds before a request is given up on. Defaults to 10. */
20
+ timeoutMs?: number;
21
+ }
22
+ export interface DoctorReport {
23
+ ok: boolean;
24
+ host: string;
25
+ checks: DoctorCheck[];
26
+ /** Set when the test trace was accepted. Search for it in your dashboard. */
27
+ runId?: string;
28
+ }
29
+ /** Shows enough of a key to recognise it without leaking it. */
30
+ export declare function maskKey(apiKey: string): string;
31
+ /** Runs every setup check in order and never throws. */
32
+ export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
33
+ export declare function formatReport(report: DoctorReport): string;
package/dist/doctor.js ADDED
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.maskKey = maskKey;
4
+ exports.runDoctor = runDoctor;
5
+ exports.formatReport = formatReport;
6
+ const index_1 = require("./index");
7
+ function keyRegion(apiKey) {
8
+ if (/^(uncoil|tracify)_sk_live_eu_/.test(apiKey))
9
+ return "eu";
10
+ if (/^(uncoil|tracify)_sk_live_us_/.test(apiKey))
11
+ return "us";
12
+ if (/^(uncoil|tracify)_sk_live_/.test(apiKey))
13
+ return "eu";
14
+ return undefined;
15
+ }
16
+ /** Shows enough of a key to recognise it without leaking it. */
17
+ function maskKey(apiKey) {
18
+ if (apiKey.length <= 12)
19
+ return "***";
20
+ return `${apiKey.slice(0, 16)}…${apiKey.slice(-4)}`;
21
+ }
22
+ function id() {
23
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
24
+ }
25
+ function errorText(error) {
26
+ const err = error;
27
+ if (err?.name === "TimeoutError" || err?.name === "AbortError")
28
+ return "the request timed out";
29
+ return err?.cause?.code || err?.cause?.message || err?.message || String(error);
30
+ }
31
+ /** Runs every setup check in order and never throws. */
32
+ async function runDoctor(options = {}) {
33
+ const checks = [];
34
+ const add = (check) => { checks.push(check); return check; };
35
+ const timeoutMs = options.timeoutMs ?? 10000;
36
+ const apiKey = (options.apiKey || (process.env.UNCOIL_API_KEY ?? process.env.TRACIFY_API_KEY) || "").trim();
37
+ const envRegion = (process.env.UNCOIL_REGION ?? process.env.TRACIFY_REGION)?.trim().toLowerCase();
38
+ const wanted = (options.region || (envRegion === "eu" || envRegion === "us" ? envRegion : undefined));
39
+ const found = keyRegion(apiKey);
40
+ const host = (options.host || (process.env.UNCOIL_HOST ?? process.env.TRACIFY_HOST) || index_1.UNCOIL_REGION_HOSTS[wanted ?? found ?? "eu"]).replace(/\/$/, "");
41
+ const report = () => ({ ok: !checks.some((c) => c.status === "fail"), host, checks, runId });
42
+ let runId;
43
+ const nodeMajor = Number(process.versions?.node?.split(".")[0]);
44
+ if (typeof fetch !== "function" || nodeMajor < 18) {
45
+ add({ id: "runtime", title: "Runtime", status: "fail", message: `This runtime has no built-in fetch (Node ${process.versions?.node ?? "unknown"}).`, hint: "Use Node 18 or newer." });
46
+ return report();
47
+ }
48
+ add({ id: "runtime", title: "Runtime", status: "ok", message: `Node ${process.versions.node}` });
49
+ if (!apiKey) {
50
+ add({ id: "key", title: "API key", status: "fail", message: "No API key was found.", hint: "Set the UNCOIL_API_KEY environment variable, or run with --key. Create a key on the API keys page of your dashboard." });
51
+ return report();
52
+ }
53
+ if (!/^(uncoil|tracify)_sk_/.test(apiKey)) {
54
+ add({ id: "key", title: "API key", status: "fail", message: `${maskKey(apiKey)} does not look like an Uncoil key (they start with uncoil_sk_).`, hint: "Copy the key again from the API keys page; make sure no other service's key was pasted by mistake." });
55
+ return report();
56
+ }
57
+ add({ id: "key", title: "API key", status: "ok", message: `Found ${maskKey(apiKey)}` });
58
+ try {
59
+ new URL(host);
60
+ }
61
+ catch {
62
+ add({ id: "region", title: "Region and host", status: "fail", message: `The host "${host}" is not a valid web address.`, hint: 'Use a full address such as "https://eu.cloud.uncoil.dev", or drop the host option and use --region eu or us.' });
63
+ return report();
64
+ }
65
+ const hostRegion = (Object.entries(index_1.UNCOIL_REGION_HOSTS).find(([, value]) => value === host)?.[0]);
66
+ if (found && hostRegion && found !== hostRegion) {
67
+ add({ id: "region", title: "Region and host", status: "fail", message: `This key belongs to ${found.toUpperCase()}, but you are pointing at ${hostRegion.toUpperCase()}.`, hint: `Set UNCOIL_REGION=${found} (or run with --region ${found}), or create a key in the ${hostRegion.toUpperCase()} region.` });
68
+ return report();
69
+ }
70
+ add({ id: "region", title: "Region and host", status: "ok", message: hostRegion ? `${hostRegion.toUpperCase()} · ${host}` : `Custom host · ${host}` });
71
+ const proxyNote = process.env.HTTPS_PROXY || process.env.https_proxy ? " A proxy is set in your environment, and Node's fetch does not use it automatically; allow this host or route the request through your proxy." : "";
72
+ try {
73
+ const response = await fetch(host, { method: "GET", redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
74
+ add({ id: "network", title: "Network", status: "ok", message: `Reached ${host} (HTTP ${response.status}).` });
75
+ }
76
+ catch (error) {
77
+ add({ id: "network", title: "Network", status: "fail", message: `Could not reach ${host} (${errorText(error)}).`, hint: `Check your internet connection, firewall and the host setting.${proxyNote}` });
78
+ return report();
79
+ }
80
+ // Which project does this key send to? Older servers have no such route, so anything unexpected is skipped quietly.
81
+ const expectedProject = (options.projectId || (process.env.UNCOIL_PROJECT_ID ?? process.env.TRACIFY_PROJECT_ID) || "").trim();
82
+ try {
83
+ const response = await fetch(`${host}/api/sdk/whoami`, { headers: { Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(timeoutMs) });
84
+ const data = response.ok ? await response.json() : null;
85
+ if (data?.projectId) {
86
+ if (expectedProject && expectedProject !== data.projectId) {
87
+ add({ id: "project", title: "Project", status: "fail", message: `UNCOIL_PROJECT_ID points at a different project than this key ("${data.projectName ?? data.projectId}").`, hint: "Run `npx @uncoil.dev/sdk login` in this folder again so the key and project are saved together." });
88
+ return report();
89
+ }
90
+ add({ id: "project", title: "Project", status: "ok", message: `Traces will land in: ${data.projectName ?? data.projectId}` });
91
+ }
92
+ }
93
+ catch { /* the trace check below reports connection problems */ }
94
+ if (options.send === false) {
95
+ add({ id: "trace", title: "Test trace", status: "skip", message: "Skipped (--no-send)." });
96
+ return report();
97
+ }
98
+ const spanRunId = `doctor_${id()}`;
99
+ const started = Date.now();
100
+ const span = {
101
+ spanId: `doctor_span_${id()}`,
102
+ runId: spanRunId,
103
+ traceName: "Uncoil setup check",
104
+ spanType: "llm_call",
105
+ modelId: "uncoil-doctor",
106
+ createdAt: new Date(started).toISOString(),
107
+ latencyMs: 1,
108
+ input: "Setup check from uncoil doctor",
109
+ output: "If you can see this trace in your dashboard, the SDK is set up correctly.",
110
+ tags: ["uncoil-doctor"],
111
+ metadata: { source: "uncoil doctor", sdk: "ts" },
112
+ };
113
+ try {
114
+ const response = await fetch(`${host}/api/ingest`, {
115
+ method: "POST",
116
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
117
+ body: JSON.stringify(span),
118
+ signal: AbortSignal.timeout(timeoutMs),
119
+ });
120
+ const status = response.status;
121
+ if (response.ok) {
122
+ runId = spanRunId;
123
+ add({ id: "trace", title: "Test trace", status: "ok", message: `Accepted. Look for "Uncoil setup check" (run ${spanRunId}) in your dashboard.` });
124
+ }
125
+ else if (status === 401) {
126
+ add({ id: "trace", title: "Test trace", status: "fail", message: "Uncoil rejected the API key (401).", hint: "The key is wrong, was revoked, or belongs to another region. Create a fresh one on the API keys page." });
127
+ }
128
+ else if (status === 403) {
129
+ add({ id: "trace", title: "Test trace", status: "fail", message: "This key is not allowed to send data (403).", hint: "Check that the key belongs to the project you expect and has not been disabled." });
130
+ }
131
+ else if (status === 402 || status === 429) {
132
+ add({ id: "trace", title: "Test trace", status: "fail", message: `Uncoil is not taking data right now (${status}).`, hint: "You may be over your plan's limit or sending too fast. Check usage on the billing page, then try again." });
133
+ }
134
+ else if (status >= 500) {
135
+ add({ id: "trace", title: "Test trace", status: "fail", message: `Uncoil had a problem on its side (${status}).`, hint: "Your app is not affected. Check the status page and try again in a minute." });
136
+ }
137
+ else {
138
+ add({ id: "trace", title: "Test trace", status: "fail", message: `Uncoil did not accept the test trace (${status}).`, hint: "Update uncoil to the latest version; if it still happens, contact support." });
139
+ }
140
+ }
141
+ catch (error) {
142
+ add({ id: "trace", title: "Test trace", status: "fail", message: `The test trace could not be sent (${errorText(error)}).`, hint: `Check your connection and try again.${proxyNote}` });
143
+ }
144
+ return report();
145
+ }
146
+ const MARKS = { ok: "✓", warn: "!", fail: "✗", skip: "-" };
147
+ function formatReport(report) {
148
+ const lines = ["Uncoil setup check", ""];
149
+ for (const check of report.checks) {
150
+ lines.push(`${MARKS[check.status]} ${check.title}: ${check.message}`);
151
+ if (check.hint && check.status !== "ok")
152
+ lines.push(` Fix: ${check.hint}`);
153
+ }
154
+ lines.push("", report.ok ? "All good. Your setup is ready to send traces." : "Something needs fixing; see the Fix lines above.");
155
+ return lines.join("\n");
156
+ }