@introspection-sdk/introspection-browser
v0.23.0
Published
Introspection observability SDK for the browser
Readme
@introspection-sdk/introspection-browser
Browser SDK for Introspection — track events, feedback, and user identity with localStorage persistence.
Install
pnpm add @introspection-sdk/introspection-browserUsage
import { IntrospectionClient } from "@introspection-sdk/introspection-browser";
const client = new IntrospectionClient({
token: "intro_xxx",
});
// Set identity once
client.identify("user_123", { email: "[email protected]" });
// Track events
client.track("Button Clicked", { buttonId: "submit" });
// Track feedback
client.feedback("thumbs_up", { comments: "Very helpful response" });
client.feedback("thumbs_down", {
previousResponseId: "msg_123",
comments: "Off topic",
});Client-side API (/api)
The @introspection-sdk/introspection-browser/api entry point lets a
single-page app create and stream Introspection tasks directly from the
browser, with no API key in JavaScript. Authentication is the standard B2B2C
flow (see the auth example):
The browser talks only to the Data Plane. Runtime resolution is a Control Plane call and stays on your backend, so the CP never has to serve CORS to customer web origins.
- The app's own backend ("broker") mints an Introspection access token —
via RFC 8693 token-exchange of the partner IdP token, a PKCE
authorization_code, orclient_credentials. The IdP secret never leaves the backend. The backend returns{ token, runtimeId, dpUrl }: it resolves the runtime id server-side (e.g. with the Node SDK'sclient.runtimes.resolve("support-agent")) and supplies the Data Plane URL, so the SPA needs no Introspection config of its own. client.connect()redeems the token at the Data PlanePOST /v1/oauth/exchangefor an HttpOnly session cookie, named for the token's environment lane (see Environments).client.tasks.start({ runtime_id })and friends ride that cookie against the Data Plane for tasks, files, conversations, and shares.
import { IntrospectionApiClient } from "@introspection-sdk/introspection-browser/api";
// Your backend returns { token, runtimeId, dpUrl }: it mints the access token,
// resolves the runtime id, and supplies the DP URL — so the browser never
// calls the CP and carries no Introspection config of its own.
const { token, runtimeId, dpUrl } = await fetch(
"/api/introspection/session",
).then((r) => r.json());
const client = new IntrospectionApiClient({
dpUrl,
getToken: () => token,
});
await client.connect(); // -> intro_dp_<environment> session cookie
const run = await client.tasks.start({
prompt: "Summarize my latest order",
runtime_id: runtimeId,
idle_timeout_seconds: 120, // idle window before the sandbox is torn down
});
for await (const ev of run.stream()) {
console.log(ev.type);
}client.tasks exposes the full CRUD surface (create / start / get /
list / update / delete / archive / unarchive) plus per-run streaming
(run.stream() yields AG-UI events, run.text(), and run.cancel(options)).
Cancellation defaults to abort; pass mode: "drain" and an optional
drain_within_seconds for graceful teardown.
create and start accept idle_timeout_seconds (number) to override
the interactive idle window before the sandbox is torn down. 0 tears it down
as soon as it's provisioned; omit to use the deployment default. Clamped to the
task timeout. When no runtime_id is supplied, pass agent_name to select a
named recipe agent instead.
Environments
The session cookie is named for its environment lane —
intro_dp_development, intro_dp_staging, intro_dp_production — so an app
running more than one lane holds a live session for each in one browser. A
single shared name gave the browser one slot, so a second connect() evicted
the first.
You never pass the environment in. Your backend picks the lane when it
mints the token; the Data Plane reads it off the token's claim and returns it,
and the client sends it back as x-introspection-environment so the right
cookie is resolved:
const dev = new IntrospectionApiClient({
dpUrl,
getToken: () => mintToken({ environment: "development" }),
});
const prod = new IntrospectionApiClient({
dpUrl,
getToken: () => mintToken({ environment: "production" }),
});
await Promise.all([dev.connect(), prod.connect()]);
dev.environment; // "development" -> cookie intro_dp_development
prod.environment; // "production" -> cookie intro_dp_production
await dev.tasks.list(); // x-introspection-environment: development
await prod.tasks.list(); // x-introspection-environment: productionclient.environment is undefined until connect() resolves. The optional
environment constructor option sends the header before that first exchange;
if it disagrees with the token, connect() throws rather than silently
preferring one — the token is the authority.
Requires a Data Plane that returns
environmentfrom/v1/oauth/exchange. Against an older deploymentclient.environmentstaysundefined, no header is sent, and a single-lane app keeps working unchanged.
Files and conversations
The same cookie session also reaches /v1/files and (read-only)
/v1/conversations on the Data Plane:
// Files — CRUD + upload/download, all identity-scoped
await client.files.upload({ file: new Blob(["hi"]), name: "hi.txt" });
const page = await client.files.list();
const bytes = await client.files.download(page.records[0].id);
// Conversations — read-only projection over the telemetry store
for await (const summary of client.conversations.list()) {
console.log(summary.conversation_id);
}
// Resolve the latest turn of a conversation (Responses-API shape)
const turn = await client.conversations.retrieve(conversationId);
console.log(turn?.output_messages);
// Complete exports — server-owned pagination, buffered or raw streaming
const json = await client.conversations.exportJson(conversationId);
const stream = await client.conversations.exportStream(conversationId, "json");client.files covers the same file surface (list / upload /
createText / get / update / delete / download / downloadStream,
plus files.versions). client.conversations mirrors ConversationsApi
(list, retrieve, exportJson, exportArrow, exportTrajectory,
exportStream, and conversations.items.list() / .get()). Both list
helpers return a Paginator — await it for the first page or for await it
to auto-page.
CORS: the browser only calls the Data Plane, so just the selected Data Plane needs to allow the SPA origin. The Control Plane never receives browser requests — runtime resolution happens on your backend.
