@introspection-sdk/introspection-node
v0.45.0
Published
Introspection observability SDK for Node.js
Readme
@introspection-sdk/introspection-node
Node.js platform SDK for Introspection — open runtimes, drive tasks, and manage experiments, recipes, repositories, members, automations, issues, app connections, files, conversations, shares, project administration (API keys, Applications, credentials, variables, integrations, judges) and eval images and task commands.
Install
pnpm add @introspection-sdk/introspection-nodeFor OTel features (analytics, traces, instrumentors), also install the peer dependencies:
pnpm add @opentelemetry/api @opentelemetry/api-logs \
@opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node \
@opentelemetry/sdk-logs @opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/exporter-logs-otlp-proto @opentelemetry/resources \
@opentelemetry/semantic-conventions @opentelemetry/context-async-hooks \
@opentelemetry/coreIntrospection API (runtimes, tasks, files)
The main Introspection API surface. No OTel packages required.
import { IntrospectionClient } from "@introspection-sdk/introspection-node";
const client = new IntrospectionClient();
const runner = await client.runtimes("customer-agent").run({
agent_name: "support-agent",
scope: "tasks:read tasks:write files:read files:write",
});
const run = await runner.tasks.start({
prompt: "Say hello in one sentence.",
});
for await (const event of run.stream()) {
console.log(event.type);
}
await runner.close();
await client.shutdown();Runner creation also accepts identity, caller, and ttl_seconds, and the
resolved runner.context exposes the current runtime or experiment context.
run.cancel() aborts by default. Pass { mode: "abort" } to make that
explicit, or { mode: "drain", drain_within_seconds: 60 } for graceful
teardown.
Interrupted runs resume through
runner.tasks.runs.resume(taskId, { resume: entries }).
identity.metadata sets key: value labels on the customer member the
identity names. It grants nothing, so unlike identity.tags (which seed a new
member only) it merges into an existing member too: asserted keys overwrite
keys of the same name and the rest are kept. Absent or {} changes nothing.
const runner = await client.runtimes("customer-agent").run({
identity: { user_id: "u_42", metadata: { plan: "enterprise" } },
});Data Plane resources: client and runner
IntrospectionClient and Runner expose the same Data Plane namespaces, and
both implement the exported DataPlaneResources interface: tasks (with
tasks.runs and tasks.commands), files, conversations, events,
metrics, shares, automations, issues, images and connections. So does the DataPlaneClient that
AuthClient.dataPlane() returns. Code written against DataPlaneResources
runs against any of them.
import type { DataPlaneResources } from "@introspection-sdk/introspection-node";
async function connectedApps(dp: DataPlaneResources) {
return (await dp.connections.list()).records.map((c) => c.app);
}
await connectedApps(client); // the client's own token, on its Data Plane
await connectedApps(runner); // the runner's token, on the runner's Data PlaneThe difference is the credential. A runner sends its session token, scoped to
the runtime and identity it was opened for; the client sends its own token,
so the server decides per call what that credential may do. A runner a member
opens for themself carries automations:read and automations:write, so
runner.automations manages that member's automations (a platform change in
progress; until it ships those routes refuse a runner with a 403).
Members
client.members lists, reads, creates and updates members on the Control
Plane's /v1/members; removing one is an org-admin action left to the CLI. A
member's tags are access-bearing (it receives every share whose granted_tag
it holds) and writable only with members:manage. A member can also still read
and write any file or task whose tags intersect its own, but that implicit tag
access is deprecated: share with a cohort through a tag share instead. Its metadata is a
map of string labels that grants nothing; keys are [A-Za-z0-9_-]+, values are
non-empty strings, at most 64 entries.
const ada = await client.members.create({
email: "[email protected]",
name: "Ada Lovelace",
metadata: { plan: "enterprise" },
});
// `tag` and `metadata` narrow the list; metadata pairs are ANDed (at most 16).
for await (const member of client.members.list({
member_type: "customer",
metadata: { plan: "enterprise", region: "eu" },
})) {
console.log(member.id, member.metadata);
}
// PATCH replaces `tags` and `metadata` wholesale; `{}` clears metadata.
await client.members.update(ada.id, { metadata: { plan: "team" } });Seeding tags on create needs members:manage as well as members:write.
Automations
client.automations (and dataPlaneClient.automations) lists, reads,
creates, updates and deletes the project's automations on the Data Plane's
/v1/automations, and trigger(id) runs one now. Reading needs
automations:read and writing automations:write; each record's can_manage
says whether the caller may change it. An automation is a scheduled prompt (kind: null) or platform work
(observation_synthesis, observation_clustering, project_check_in). A
cron automation fires on cron_schedule, or on metadata.cron_schedules in
metadata.timezone; a one-off reminder is a manual automation with a future
next_trigger_at. Set task_id to post each firing into an existing task
instead of creating one.
The server serves these routes to administrators only today (a 403 otherwise).
introspection-cloud#3137, not yet released, opens them to members for their own
automations that post into one of their tasks; until it ships, the task_id
list filter is ignored by the server.
const reminder = await client.automations.create({
name: "Friday check-in",
trigger_type: "manual",
prompt: "How did the week go?",
runtime_group_id: runtimeGroupId,
task_id: taskId,
next_trigger_at: "2026-10-09T09:00:00Z",
});
// Recurring: one or more cron schedules in a timezone.
await client.automations.create({
name: "Weekday digest",
trigger_type: "cron",
cron_schedule: "0 9 * * 1-5",
metadata: {
cron_schedules: ["0 9 * * 1-5"],
timezone: "America/Los_Angeles",
},
prompt: "Summarize yesterday's conversations.",
runtime_group_id: runtimeGroupId,
});
// PATCH sends only the fields you set; `metadata` replaces wholesale.
await client.automations.update(reminder.id, { enabled: false });
for await (const automation of client.automations.list({ task_id: taskId })) {
console.log(automation.id, automation.next_trigger_at, automation.can_manage);
}
const { status, task_id } = await client.automations.trigger(reminder.id);A firing clears a manual automation's slot; re-arm it with
update(id, { next_trigger_at }). A cron automation derives its own slot and
rejects one set by the client.
Each task-run trigger is recorded as an introspection.automation.triggered
event, and a scheduled slot that ran nothing as
introspection.automation.skipped (with a reason such as
conditions_not_met or target_busy; readable only with project-wide
telemetry access). Read them with client.events.list, filtered by
automation_id or task_id:
const page = await client.events.list({
event_name: "introspection.automation.triggered",
automation_id: reminder.id,
});
for (const ev of page.records)
console.log(ev.payload.task_id, ev.payload.posted);Issues
client.issues (and runner.issues, dataPlaneClient.issues) lists, reads,
creates, updates and deletes issues on the Data Plane's /v1/issues. An issue
is a brief on a fixed worker task: a title, a description, a priority and a
status, the evidence it cites (files, links, events and spans) and its open
human requests. Reading needs issues:read, creating and updating
issues:write, and deleting issues:delete.
const issue = await client.issues.create({
task_id: taskId,
title: "Checkout times out for EU customers",
description: "Three conversations hit a 30s timeout on /checkout.",
priority: "high",
tags: ["customer:acme"],
links: [{ url: "https://github.com/acme/app/pull/7" }],
});
// Repeatable filters take one value or several (ORed); metadata pairs are ANDed.
for await (const open of client.issues.list({
status: ["open", "waiting"],
owner: "project",
metadata: { region: "eu" },
})) {
console.log(open.display_index, open.title, open.open_requests.length);
}
// Edit the brief. Omitted fields are unchanged, and each list replaces its
// list wholesale, so `links: []` clears the links. `ifMatch` is optional:
// with the issue's ETag, an edit against a stale read is refused with 412.
await client.issues.update(
issue.id,
{ status: "closed", links: [] },
{ ifMatch: issueEtag(issue) },
);
// Or create one human request with a new UUIDv7 `id`; the same `id` later
// changes or resolves it.
await client.issues.update(issue.id, {
request: {
id: requestId,
question: "Can we raise the gateway timeout?",
assignee_id: memberId,
},
});
await client.issues.delete(issue.id);issueEtag(issue) is the ETag the Data Plane returns for an issue (its
updated_at exactly as the API renders it, quoted). A brief edit and a delete take an
optional { ifMatch }, sent as If-Match; a stale one is refused with 412
(an IntrospectionAPIError with status 412), and leaving it out writes
without the check. A request mutation takes no ifMatch: a new request id creates the
request and an existing one changes it, last write wins.
Project administration
client.apiKeys, client.applications (with .idps, .secrets and
.keys), client.credentials, client.variables, client.integrations,
client.judges, client.projects (with read-only .peerings),
client.organizations.me(), client.oidc.me() and client.tokens.create()
(a Data Plane token for one project) cover the Control Plane's project
administration. They need an org credential,
such as a member's Control Plane session (cpSession); a project API key
carries none of their scopes. Project-scoped methods take an optional
project (slug or id) for a credential that names no project.
const client = new IntrospectionClient({ cpSession: process.env.CP_SESSION });
const created = await client.apiKeys.create({ name: "ci" }, "my-project");
saveSecret(created.api_key); // returned once
await client.apiKeys.acknowledge(created.id, "my-project");
// Rotate: keep the old key working for an hour.
await client.apiKeys.delete(created.id, {
project: "my-project",
expires_in: 3600,
});client.integrations.accessToken(id, { type, permissions }) mints a
short-lived provider token (at most an hour, never stored) for an
integration (integrations:token). GitHub is the only type today, capped at
{ metadata: "read" }; a request beyond the cap is refused with a 403 whose
code is integration_token_cap_exceeded, never narrowed.
const { token, expires_at } = await client.integrations.accessToken(
integration.id,
{ type: "github", permissions: { metadata: "read" } },
);client.integrations.repositories(id) lists every repository a GitHub
integration's installation can reach, in one response (integrations:read).
The Control Plane reads them with its own credential, so no token reaches
the caller.
const { records } = await client.integrations.repositories(integration.id);
for (const repo of records) console.log(repo.full_name, repo.default_branch);client.projects.peerings reads a project's runtime peerings, the runtime
groups it offers to other projects (projects:read). Creating or revoking a
peering is a person's act in the web app, so the SDK has no write methods.
// Offered by my-project, optionally narrowed to one runtime group.
const offered = await client.projects.peerings.list(
"my-project",
"support-bot",
);
// Offered to my-project by other projects.
for await (const p of client.projects.peerings.granted("my-project"))
console.log(p.project_id, p.runtime_group_id);
const one = await client.projects.peerings.get("my-project", peeringId);Eval images and task commands
images builds or mirrors the images an eval task's compose services run as,
and tasks.commands runs exec / upload / download against an eval task's
live sandbox (a task created with kind: "eval" and commands: true). Both
need an admin, owner or operator credential on a plane with evals enabled.
const image = await client.images.create({
name: "hello-mcp/mcp-server",
source: { kind: "mirror", reference: "ghcr.io/acme/mcp@sha256:…" },
});
const command = await client.tasks.commands.create(taskId, {
kind: "exec",
command: ["pytest", "-q"],
});
// Holds the read up to MAX_COMMAND_WAIT_SECONDS for the command to finish.
const done = await client.tasks.commands.get(taskId, command.id, {
wait_seconds: 30,
});
console.log(done.status, done.exit_code, done.stdout);Connections
connections lists, reads, creates and deletes the apps members connected for
themselves, on the Data Plane's /v1/connections. A caller who is not an
administrator always sees and manages only their own. Reads need
connections:read, create needs connections:write and delete needs
connections:delete. These are a different resource from the Control Plane's
client.connectors.connections, which are nested under a connector.
create returns a single-use connect page; send the member to its
authorize_url, and the connection exists once they finish there. On a runner,
runtime defaults to the runner's runtime group. On the client, pass it.
const { authorize_url } = await runner.connections.create({ app: "gmail" });
for await (const connection of runner.connections.list({ app: "gmail" })) {
console.log(connection.id, connection.account_name, connection.healthy);
}
// Administrators can list another member's connections.
await client.connections.list({ member_id: memberId });
await runner.connections.delete(connectionId);Native email-code sign-in
AuthClient signs in the end users of a native Application with emailed
one-time codes, shaped like the Supabase Auth client. The session belongs to a
customer member of the Application's organization, is scoped to one project,
and carries the Application's allowed_scopes.
import { AuthClient } from "@introspection-sdk/introspection-node";
const auth = new AuthClient({
clientId: "intro_app_…", // a `native` Application
project: "my-project",
storage: sessionStore, // getItem / setItem / removeItem, sync or async
});
await auth.signInWithOtp({ email });
await auth.verifyOtp({ email, token: code });
const dp = await auth.dataPlane();
const run = await dp.tasks.start({ prompt: "Hello", runtime_id: runtimeId });
console.log(await run.text());- Codes are opaque. A returning user's code is 6 digits; a new user's first
code is 6 characters of A–Z and 0–9. Do not restrict the input to digits.
A wrong, expired or reused code throws
ValidationErrorwithcode === "invalid_grant"; requesting codes too often throwsRateLimitErrorwithretryAfter. - Refresh.
getSession()renews the session withinleewaySeconds(default 60) of expiry, and every request made throughauth.dataPlane()renews it once after a401; concurrent renewals share one refresh. A refresh the server rejects signs the user out and throwsAuthenticationError; a network failure keeps the session. - Superseded responses. A sign-in that resolves after a sign-out or a newer
sign-in rejects with an
AbortErrorinstead of overwriting the newer state. - Events.
onAuthStateChange((event, session) => …)reportsINITIAL_SESSION,SIGNED_IN,TOKEN_REFRESHEDandSIGNED_OUT. - Data Plane only. The token is a Data Plane credential, so the Control
Plane routes on
IntrospectionClient(runtimes, connectors, members) reject it. Pass the runtime asruntime_idwhen creating a task.auth.dataPlane()returns aDataPlaneClient, which implementsDataPlaneResources:tasks,files,conversations,shares,events,metrics,automations,issuesandconnections(wherecreatetakes theruntime). - Sign-out clears the local session, then revokes it with
POST /v1/oauth/revoke; the local session is gone even if revocation fails.
See examples/api/native-email-code.ts.
Federated sign-in through your own IdP (a jwks Application) keeps its session
in that IdP's SDK; exchange its token with tokenExchange() instead.
Device-code login
DeviceCodeAuth runs the login introspection login does (RFC 8628): request
a code, show it to the member, and poll until they approve in the browser.
import { DeviceCodeAuth } from "@introspection-sdk/introspection-node";
const login = new DeviceCodeAuth();
const device = await login.requestCode({ project: "my-project" });
console.log(`Open ${device.verification_uri} and enter ${device.user_code}`);
const token = await login.waitForToken(device);
const renewed = await login.refresh({
refreshToken: token.refresh_token!,
sessionId: token.session_id!,
orgId: token.org_id!,
});
const dev = await login.developmentCredential(renewed.access_token);
await login.revoke({
sessionId: token.session_id!,
orgId: token.org_id!,
token: renewed.refresh_token ?? token.refresh_token!,
});revoke() ends this login alone; oidcLogout({ cpSession }) signs the member
out of every session, their browser's included. pollToken() makes one poll
and reports pending, slow_down, denied, expired or approved. client.remotes.connectDocs() reads the protocol of
the /v1/remotes/connect WebSocket that introspection dev dials with such a
login.
Repositories
client.repositories looks up a project's linked repositories on the Control
Plane and reads their contents through the Data Plane (repositories:read).
const [repo] = await client.repositories.list({
project: "demo",
slug: "acme/agent",
});
// Every entry of a directory; pages are followed automatically.
for await (const entry of client.repositories.contents(repo.id, {
path: "src",
ref: "main",
})) {
console.log(entry.type, entry.path);
}
// One path: a directory page or a file, discriminated on `type`.
const readme = await client.repositories.contents.get(repo.id, "README.md");
if (readme.type === "file") console.log(readme.encoding, readme.content);Enumerating a path that is a file throws a ValidationError; read it with
contents.get() instead.
History reads the same way: commits() pages back from a branch, tag or sha
(the default branch when omitted), and commit() returns one commit with its
changed files and unified diff.
for await (const commit of client.repositories.commits(repo.id, {
sha: "main",
path: "src",
})) {
console.log(commit.sha, commit.message);
}
const detail = await client.repositories.commit(repo.id, "abc123");
console.log(
detail.files.map((file) => file.filename),
detail.patch,
);merge() merges a branch or commit sha into a branch, like GitHub's merges API
(repositories:write). It resolves to the merge commit, or null when base
already contains head; a conflict throws a ConflictError and changes
nothing.
const merge = await client.repositories.merge(repo.id, {
base: "main",
head: "feature",
});
console.log(merge?.sha ?? "already up to date");Annotations
Annotations capture what a domain expert found good or bad on an OTel span. They
are labels, comments, and assignments—not numeric scores. Writes require an
authenticated business-member token with annotations:write; project API
keys and sandbox credentials cannot write annotations.
const client = new IntrospectionClient({
token: memberAccessToken,
cpSession: encodedMemberSession,
advanced: {
baseApiUrl: "https://api.introspection.dev", // Control Plane
dpUrl: memberDataPlaneUrl,
},
});
const span = {
trace_id: "0123456789abcdef0123456789abcdef",
span_id: "0123456789abcdef",
};
await client.tags.create({
kind: "annotation",
name: "strong-structure",
color: "#f97316",
description: "A useful structure to preserve during distillation",
});
await client.annotations.create(span, { labels: ["strong-structure"] });
await client.annotations.create(span, {
comment: "Keep the conclusion before the evidence.",
});
await client.annotations.create(span, {
reviewerEmails: ["[email protected]"],
});
await client.annotations.create(span, { reviewerEmails: [] });Labels and reviewers are complete snapshots; pass [] to clear one. Comments
append. Every mutation gets a UUIDv7 event_id before its
first transport attempt, so an automatic retry remains one event. Supply
{ event_id } as the final method argument when retrying across processes.
client.annotations.list() and client.tags.list("annotation") are both
awaitable for the first page and async-iterable across every cursor page.
Tags come in two catalogues — annotation tags (the labels above, each with a
color) and access tags (the values shares and member tags carry, which have
no color) — so list takes the kind, and get, update and delete take the
kind and the name.
Read immutable annotation history with
client.events.list({ event_name: "introspection.annotation", trace_id, span_id }).
Pi instrumentation
Pi is the supported agent-instrumentation path:
pnpm add @earendil-works/pi-agent-core @earendil-works/pi-aiimport * as introspection from "@introspection-sdk/introspection-node/otel";
import { Agent } from "@earendil-works/pi-agent-core";
import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all";
await introspection.init({ serviceName: "my-app" });
const agent = new Agent({
initialState: {
model: getBuiltinModel("anthropic", "claude-sonnet-4-6"),
systemPrompt: "You are a helpful support agent.",
},
});
introspection.instrumentPi(agent, {
conversationId: "conv_123",
agentId: "support-agent",
agentName: "Support",
});
await agent.prompt("Help me understand my latest invoice.");
await introspection.shutdown();Both import styles work:
import {
init,
conversation,
track,
} from "@introspection-sdk/introspection-node/otel";Dual export
Build the OpenTelemetry provider yourself with both span processors:
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { IntrospectionSpanProcessor } from "@introspection-sdk/introspection-node/otel";
const provider = new NodeTracerProvider({
spanProcessors: [
new IntrospectionSpanProcessor({ token: process.env.INTROSPECTION_TOKEN }),
new BatchSpanProcessor(otherBackendExporter),
],
});
provider.register();
await introspection.init({ tracerProvider: provider });IntrospectionSpanProcessor exports its own converted copy of each span, so the vendor processor receives the raw span and processor order is irrelevant. For a quick alternative: init({ spanProcessors: [new BatchSpanProcessor(otherBackendExporter)] }).
Pi is the only framework with a built-in integration. init() detects it and
wires it automatically; to instrument an Agent by hand, import
IntrospectionPiInstrumentor from
@introspection-sdk/introspection-node/otel/pi. It lives on its own subpath
because it reaches into @earendil-works/pi-ai at runtime, and the /otel
barrel must stay importable without it.
Analytics events (track, feedback, identify)
import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
const logs = new IntrospectionLogs({
token: process.env.INTROSPECTION_TOKEN,
serviceName: "my-service",
});
await logs.withUserId("user_123", async () => {
await logs.withConversation("conv_456", "msg_123", async () => {
logs.feedback("thumbs_up", { comments: "Great response!" });
});
});
logs.track("Button Clicked", { buttonId: "submit" });
logs.identify("user_123", { email: "[email protected]" });
await logs.shutdown();Methods
| Method | Description |
| --------------------------------------- | -------------------------------------------------- |
| logEvent(name, attributes?, options?) | Log an app event under any custom name (see below) |
| track(event, properties?) | Track any user action (an alias of logEvent) |
| feedback(type, options?) | Track feedback on AI responses |
| identify(userId, traits?) | Associate a user with traits |
| flush() | Flush pending events |
| shutdown() | Shutdown and flush |
Context helpers (OTel baggage)
| Method | Description |
| ---------------------------------------------- | ---------------------------- |
| withUserId(id, callback) | Set user context |
| withConversation(id?, responseId?, callback) | Set conversation context |
| withAgent(name, id?, callback) | Set agent context |
| withAnonymousId(id, callback) | Set anonymous ID |
| withBaggage(values, callback) | Set arbitrary baggage values |
Logging custom events
logEvent(name, attributes?, options?) records an app event under a name you
choose — ark.feed.entry, checkout.completed — so the platform stores it and
you can query it back by name. track() is a thin alias of it. Attributes are
stored under properties.*.
import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
const logs = new IntrospectionLogs({ serviceName: "ark" }); // INTROSPECTION_TOKEN
logs.logEvent(
"ark.feed.entry",
{ entry_id: entry.id, source: "rss", score: 0.92 },
{
eventId: `feed-entry:${entry.id}`, // stable id: consumers dedupe on it
timestamp: entry.publishedAt, // Date or epoch ms; default now
identity: { userId: entry.ownerId }, // overrides the scoped identity
severity: "INFO", // DEBUG | INFO | WARN | ERROR
},
);
await logs.shutdown(); // flushesAfter init(), the same call is available as a module-level function:
import * as introspection from "@introspection-sdk/introspection-node/otel";
await introspection.init({ serviceName: "ark" });
introspection.logEvent("ark.feed.entry", { entry_id: "e_1" });- Names. Use your own namespace. Names starting with
introspection.(platform events) orgen_ai.(OpenTelemetry GenAI conventions) are reserved andlogEventthrows on them.track()delegates tologEvent, so it rejects them too. - Idempotency. Without
eventIdeach call gets a fresh id. When delivery can repeat (retries, replayed jobs, an agent re-running a step), pass an id derived from the thing being recorded: re-sending the sameeventIdis how consumers recognise and drop the duplicate. - Identity and context. User / anonymous id and
gen_ai.conversation.id/ agent come from the active baggage (withUserId,withConversation,withAgent);options.identityoverrides per field.
From a recipe or agent sandbox
A Runtime sandbox is started with INTROSPECTION_TOKEN and
INTROSPECTION_BASE_OTEL_URL in its environment, so a recipe tool or
agent step needs no configuration — construct the logger and flush before the
step returns:
import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
const events = new IntrospectionLogs({ serviceName: "ark-recipe" });
export async function recordFeedEntries(entries: FeedEntry[]) {
for (const entry of entries) {
events.logEvent(
"ark.feed.entry",
{ entry_id: entry.id, title: entry.title, url: entry.url },
{ eventId: `ark.feed.entry:${entry.id}` },
);
}
await events.flush(); // the sandbox may be torn down after the step
}Reading custom events back
Custom events are served by /v1/events under the introspection.track
family, whose payload carries the original name and the properties:
import { IntrospectionClient } from "@introspection-sdk/introspection-node";
const client = new IntrospectionClient();
type TrackPayload = { name: string; properties?: Record<string, unknown> };
for await (const ev of client.events.list({
event_name: "introspection.track",
lookback: "24h",
})) {
const payload = ev.payload as TrackPayload;
if (payload.name !== "ark.feed.entry") continue;
console.log(ev.timestamp, payload.properties?.entry_id);
}Filtering by name server-side is not available yet — it arrives with a
pending platform change. Until then, filter on payload.name client-side as
above.
OpenTelemetry span processor
Attach the processor to a provider you already own, or stand one up with
setupTracing. Adding a second IntrospectionSpanProcessor pointed at another
OTLP endpoint dual-exports the same spans — see
examples/otel/pi/dual-export.ts.
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { IntrospectionSpanProcessor } from "@introspection-sdk/introspection-node/otel";
const provider = new NodeTracerProvider({
spanProcessors: [
new IntrospectionSpanProcessor({ token: process.env.INTROSPECTION_TOKEN }),
],
});
provider.register();Environment variables
# Introspection API (IntrospectionClient)
export INTROSPECTION_TOKEN="intro_xxx"
export INTROSPECTION_BASE_API_URL="https://api.introspection.dev" # optional
# Development only: route this process's tasks to your own `introspection dev`
# server when several developers share one Runtime. `introspection dev` prints
# the line to copy. No default.
export INTROSPECTION_DEV_TARGET="roland" # optional
# OTel (IntrospectionLogs + IntrospectionSpanProcessor)
export INTROSPECTION_BASE_OTEL_URL="https://otel.introspection.dev" # optional
export INTROSPECTION_SERVICE_NAME="my-service" # optional
# SDK diagnostics: error, warn, info, debug, verbose. Default: warn.
export INTROSPECTION_LOG_LEVEL="debug" # optionalRunning behind a proxy
REST calls honour HTTPS_PROXY / HTTP_PROXY and NO_PROXY, which Node's
built-in fetch ignores on its own. Nothing else needs setting, apart from a
proxy that re-signs TLS with a private CA: Node only trusts that CA through
NODE_EXTRA_CA_CERTS.
export HTTPS_PROXY="http://proxy.internal:3128"
export NODE_EXTRA_CA_CERTS="/etc/ssl/certs/proxy-ca.pem" # only for a re-signing proxyAn allowlist must admit the control plane (api.introspection.dev) and your
project's data plane, which is a regional host such as
api.syd01.ap-southeast-2.aws.introspection.dev; introspection whoami prints
both. A failed call names the host it could not reach, for example
GET api.syd01.ap-southeast-2.aws.introspection.dev/v1/tasks failed: fetch failed: ….
A fetch you pass in advanced.fetch, or one installed globally after the
SDK loads, is used as is.
