JavaScript SDK
Complete JavaScript/TypeScript SDK reference.
Reference for the Risicare JavaScript/TypeScript SDK. The current published version is 0.9.0 — see the Changelog.
Installation
npm install risicareThe package has named exports only. It has no default export. Use
import { init } from 'risicare' or import * as risicare from 'risicare'.
Core Functions
init
Initialize the SDK.
import { init } from 'risicare';
init(config?: RisicareConfig): voidenable / disable / isEnabled
Runtime control.
import { enable, disable, isEnabled } from 'risicare';
enable(): void
disable(): void
isEnabled(): booleanisEnabled() is false before init(), with tracing off, with no API key, and after the SDK's own SIGTERM or SIGINT drain. enable() cannot turn tracing on when the SDK has no API key. In 0.8.0 and earlier, RISICARE_TRACING=true with no key left it true.
flush / shutdown
Export control.
import { flush, shutdown } from 'risicare';
flush(timeoutMs?: number): Promise<boolean> // default 5000
shutdown(timeoutMs?: number): Promise<void> // default 5000flush() resolves true only when the gateway acknowledged every span and every score that the SDK accepted since the previous flush() returned (or since init()), and nothing is in the queue or in flight. This holds also in a SIGTERM or SIGINT listener and after shutdown().
A loss makes the next flush() resolve false one time, and the one after it true again. A loss is a span dropped for any reason (shutdown_residue included), a span rejected or left out of an acknowledgement, and a failed score (also a score that score() refuses at the call). Each one raises a counter that names the reason: droppedSpansByReason, rejectedSpans, failedScores. A flush() that resolves false at its deadline (timeoutMs, default 5000 ms) while none of them went up since the previous flush() means that the spans are still in flight, not lost. failedExports is not a loss counter: a later round can still deliver those spans.
flush() also resolves false when the SDK has no evidence of delivery: before init(), with tracing off, and with no API key. A flush() that you call while the SDK's own drain runs (your SIGTERM or SIGINT listener, registered after init()) waits for the drain inside its own deadline. flush() only waits; to end a process in a fixed time, call await shutdown(timeoutMs). A new init() starts a new count.
In 0.8.0 and earlier, flush() resolved true before init() and with tracing off, and it could resolve true in a signal listener or after shutdown() with nothing delivered.
If your own SIGTERM or SIGINT listener makes spans that must be delivered, register it before init(). Your listener runs once, in both orders, and the exit code that it sets stands: the SDK raises the signal again, so that the default action ends the process, only when you have no listener at that moment.
getMetrics
Returns processor metrics for monitoring export health.
import { getMetrics } from 'risicare';
getMetrics(): {
sdkState: 'running' | 'shutdown' | 'uninitialised';
exportedSpans: number;
droppedSpans: number;
droppedSpansByReason: {
queue_full: number;
transport_refused: number;
shutdown_residue: number;
unacknowledged: number;
};
rejectedSpans: number;
failedScores: number;
failedExports: number;
maskErrors?: number; // absent when sdkState is 'uninitialised'
queueSize: number;
queueCapacity: number;
queueUtilization: number;
}exportedSpans counts the spans that the gateway accepted, rejectedSpans the spans that it
rejected, and failedScores the score requests that failed. sdkState: 'shutdown' marks the final
counts, kept after shutdown().
getTraceContent
Returns whether content tracing is enabled.
import { getTraceContent } from 'risicare';
getTraceContent(): booleanreportError
Report a caught exception so Risicare records it on an error span and classifies it against the error taxonomy. Root-cause diagnosis and fix generation are held for the beta, so reporting an error does not trigger them. Never throws.
import { reportError } from 'risicare';
try {
const result = await llm.invoke(query);
} catch (error) {
reportError(error); // Records and classifies the error
return fallbackResponse;
}| Parameter | Type | Default | Description |
|---|---|---|---|
error | Error | string | (required) | The caught exception |
options.name | string | error class name | Custom span name |
options.attributes | Record | {} | Additional span attributes |
score
Record a custom evaluation score on a trace. Non-blocking — sends in background via fetch(). Never throws.
import { score } from 'risicare';
score('4bf92f3577b34da6a3ce929d0e0e4736', 'factual_accuracy', 0.92, {
comment: 'Verified against source documents',
});| Parameter | Type | Default | Description |
|---|---|---|---|
traceId | string | (required) | The trace to score |
name | string | (required) | Score name |
value | number | (required) | Score between 0.0 and 1.0 |
options.spanId | string | undefined | Specific span within the trace |
options.comment | string | undefined | Human-readable explanation |
A value outside [0.0, 1.0] is not sent. The SDK logs a WARNING for it. A failed request (an error, a timeout after 2000 ms, or a status of 300 or more) adds 1 to failedScores; the first failure logs a WARNING, and failures in the next 10 seconds are counted in the next one. The SDK does not follow a redirect. The traceId must be 32 lowercase hex characters: the SDK does not check the form, and the API refuses any other form.
Provider Patches
12 native provider patches, each imported from a sub-path export. All client-based patches return a transparent ES Proxy wrapper — the original client is unchanged. patchVercelAI() is the exception — it takes no client and wraps top-level functions, returning { tracedGenerateText, tracedStreamText, tracedGenerateObject }.
| Provider | Import | Patch Function |
|---|---|---|
| OpenAI | risicare/openai | patchOpenAI(client) |
| Anthropic | risicare/anthropic | patchAnthropic(client) |
| Vercel AI | risicare/vercel-ai | patchVercelAI() |
| Google Gemini | risicare/google | patchGoogleAI(client) |
| Mistral | risicare/mistral | patchMistral(client) |
| Groq | risicare/groq | patchGroq(client) |
| Cohere | risicare/cohere | patchCohere(client) |
| Together AI | risicare/together | patchTogether(client) |
| Ollama | risicare/ollama | patchOllama(client) |
| HuggingFace | risicare/huggingface | patchHuggingFace(client) |
| Cerebras | risicare/cerebras | patchCerebras(client) |
| AWS Bedrock | risicare/bedrock | patchBedrock(client) |
import { patchOpenAI } from 'risicare/openai';
import { patchAnthropic } from 'risicare/anthropic';
import { patchGroq } from 'risicare/groq';
import OpenAI from 'openai';
const openai = patchOpenAI(new OpenAI());
// All chat.completions.create and embeddings.create calls are now tracedHost Detection (OpenAI-Compatible)
When using patchOpenAI() with a client pointing to an OpenAI-compatible API, the SDK automatically detects the real provider from the base URL and sets gen_ai.system correctly:
| Provider | Base URL |
|---|---|
| DeepSeek | api.deepseek.com |
| Together AI | api.together.xyz |
| Groq | api.groq.com |
| xAI (Grok) | api.x.ai |
| Fireworks | api.fireworks.ai |
| Baseten | inference.baseten.co |
| Novita | api.novita.ai |
| BytePlus | api.byteplus.com, ark.cn-beijing.byteplus.com |
import { patchOpenAI } from 'risicare/openai';
import OpenAI from 'openai';
// DeepSeek via OpenAI-compatible API — auto-detected as "deepseek"
const deepseek = patchOpenAI(new OpenAI({
baseURL: 'https://api.deepseek.com/v1',
apiKey: process.env.DEEPSEEK_API_KEY,
}));patchVercelAI
Returns higher-order wrapper functions for Vercel AI SDK:
import { generateText, streamText, generateObject } from 'ai';
import { patchVercelAI } from 'risicare/vercel-ai';
const { tracedGenerateText, tracedStreamText, tracedGenerateObject } = patchVercelAI();
const generate = tracedGenerateText(generateText);
const stream = tracedStreamText(streamText);
const genObject = tracedGenerateObject(generateObject);Framework Integrations
4 framework integrations, each imported from a sub-path export:
LangChain.js
import { RisicareCallbackHandler } from 'risicare/langchain';
const handler = new RisicareCallbackHandler();
const result = await chain.invoke(input, { callbacks: [handler] });LangGraph.js
import { instrumentLangGraph } from 'risicare/langgraph';
const tracedGraph = instrumentLangGraph(compiledGraph);
const result = await tracedGraph.invoke(input);Instructor
import { patchInstructor } from 'risicare/instructor';
const patchedClient = patchInstructor(instructorClient);
const result = await patchedClient.chat.completions.create({
model: 'gpt-4o',
response_model: { schema: MySchema, name: 'MySchema' },
messages: [{ role: 'user', content: 'Extract the data' }],
});LlamaIndex.TS
import { Settings } from 'llamaindex';
import { RisicareLlamaIndexHandler } from 'risicare/llamaindex';
const handler = new RisicareLlamaIndexHandler();
for (const name of RisicareLlamaIndexHandler.EVENT_NAMES) {
Settings.callbackManager.on(name as any, (e: any) => handler.onEvent(e));
}
// Provider spans are suppressed only inside withSuppression():
const result = await handler.withSuppression(() => queryEngine.query({ query: '...' }));Streaming
Trace async iterables with automatic span lifecycle management:
import { tracedStream } from 'risicare';
const stream = tracedStream(asyncIterable, { name: 'llm-stream' });
for await (const chunk of stream) {
handleChunk(chunk);
}
// Span automatically records stream.chunk_count and stream.completedDedup
Framework integrations can suppress provider-level tracing to prevent double-tracing:
import { suppressProviderInstrumentation, isProviderInstrumentationSuppressed } from 'risicare';
await suppressProviderInstrumentation(async () => {
// Provider patches are suppressed inside this scope
await client.chat.completions.create(/* ... */);
});Higher-Order Functions
agent
import { agent } from 'risicare';
agent<T extends (...args: any[]) => any>(
options: {
name?: string;
role?: string; // any string; canonical values are the AgentRole enum
agentType?: string;
version?: number;
metadata?: Record<string, unknown>;
},
fn: T
): Tsession
import { session } from 'risicare';
session<T extends (...args: any[]) => any>(
optionsOrResolver: SessionOptions | ((...args: any[]) => SessionOptions),
fn: T
): T
interface SessionOptions {
sessionId: string;
userId?: string;
}Phase Functions
Phase decorators accept either a bare function or a name + function (added in 0.2.1):
These return a wrapped function — you must call it
traceThink / traceDecide / traceAct / traceObserve do not run your
function. They return a wrapper, and a span is emitted only when you invoke
that wrapper. Writing traceAct('execute-tool', fn); on its own emits zero
spans — with no error and no warning, which reads as "tracing is broken"
rather than "the wrapper was never called".
import { traceThink, traceDecide, traceAct, traceObserve } from 'risicare';
// Bare form with an anonymous function — the span is named `phase:think`.
// With a named function, the span is `<phase>:<function>`, for example `think:analyzeQuery`.
const analyzeBare = traceThink(async () => { /* ... */ });
// Named form — explicit span name
const analyze = traceThink('analyze-query', async () => { /* ... */ });
// All four phases support both forms
const choose = traceDecide('choose-action', async () => { /* ... */ });
const execute = traceAct('execute-tool', async () => { /* ... */ });
const check = traceObserve('check-result', async () => { /* ... */ });
// Nothing is traced until the wrappers are invoked:
await analyze();
await choose();
await execute();
await check();Multi-Agent Functions
import { traceMessage, traceDelegate, traceCoordinate } from 'risicare';
traceMessage<T extends (...args: any[]) => any>(
options: { to: string },
fn: T
): T
traceDelegate<T extends (...args: any[]) => any>(
options: { to: string },
fn: T
): T
traceCoordinate<T extends (...args: any[]) => any>(
options: { participants: string[] },
fn: T
): TContext Functions
getCurrentContext
import { getCurrentContext } from 'risicare';
// Returns a plain object snapshot. session/agent/span are either an
// object or null; phase is a SemanticPhase or null.
getCurrentContext(): {
session: { sessionId: string; userId?: string } | null;
agent: {
agentId: string;
agentName?: string;
agentRole?: string;
agentType?: string;
} | null;
span: { spanId: string; traceId: string } | null;
phase: SemanticPhase | null;
}Individual Getters
import {
getCurrentTraceId,
getCurrentSpanId,
getCurrentAgentId,
getCurrentSessionId,
} from 'risicare';
getCurrentTraceId(): string | undefined
getCurrentSpanId(): string | undefined
getCurrentAgentId(): string | undefined
getCurrentSessionId(): string | undefinedW3C Trace Context
import { injectTraceContext, extractTraceContext } from 'risicare';
injectTraceContext(headers: Record<string, string>): Record<string, string>
// Always returns a record (never undefined). When the incoming headers
// carry a Risicare tracestate it includes traceId, parentSpanId, flags,
// and any propagated sessionId / agentId.
extractTraceContext(headers: Record<string, string>): Record<string, string | undefined>getTraceContext
Returns the current trace context as a serializable object. When called outside any active span it pre-allocates a root trace ID so the next span created in the same context inherits it.
import { getTraceContext } from 'risicare';
getTraceContext(): TraceContext // { traceId, spanId, sessionId?, agentId? }Context Scope Wrappers
Run a callback inside an explicit session, agent, or phase scope. Each returns the callback's result.
import { withSession, withAgent, withPhase } from 'risicare';
import { SemanticPhase } from 'risicare';
withSession(options: { sessionId: string; userId?: string }, fn: () => T): T
withAgent(options: { name?: string; role?: string; agentType?: string }, fn: () => T): T
withPhase(phase: SemanticPhase, fn: () => T): TWebhook Verification
Verify the signature of an incoming Risicare webhook delivery before processing it. verifyWebhookSignature is a root export (import { verifyWebhookSignature } from 'risicare') — there is no risicare/webhooks subpath. It is dependency-light (Node crypto only) and ships in risicare@0.4.0.
import {
verifyWebhookSignature,
WebhookVerificationError,
DEFAULT_TIMESTAMP_TOLERANCE_S, // 300 (seconds)
} from 'risicare';
verifyWebhookSignature(
payload: Uint8Array | ArrayBuffer | string, // raw request body bytes
headers: Headers | Map<string, string> | Record<string, string | string[] | undefined>,
secret: string,
opts?: { toleranceS?: number }, // default 300s skew window
): void // returns undefined on success; throws on any failureThe verifier enforces three checks (all must pass): both X-Risicare-Signature and X-Risicare-Timestamp headers present and well-formed; timestamp within toleranceS of now (replay protection); and a constant-time HMAC-SHA256 match over the canonical <timestamp>.<payload_bytes> string. Any failure throws WebhookVerificationError — catch it specifically to return a 401.
Pass the raw body, not parsed JSON
The signature is computed over the EXACT bytes received. Parsing JSON and re-serializing changes key ordering and whitespace, so the signature will not match. Use your framework's raw-body option (e.g. express.raw()).
import express from 'express';
import { verifyWebhookSignature, WebhookVerificationError } from 'risicare';
const app = express();
app.post('/risicare-webhook', express.raw({ type: '*/*' }), (req, res) => {
try {
verifyWebhookSignature(req.body, req.headers, process.env.WEBHOOK_SECRET!);
} catch (e) {
if (e instanceof WebhookVerificationError) {
return res.status(401).send(e.message);
}
throw e;
}
// ... process the verified event ...
res.status(200).end();
});Node runtime only
This verifier is synchronous and uses Node's crypto module. It does not run as-is on Cloudflare Workers / Vercel Edge / Deno Deploy. An async Web Crypto variant can be added on request.
Span Registry
import {
registerSpan,
getSpanById,
unregisterSpan,
} from 'risicare';
registerSpan(span: Span, ttlMs?: number): void
getSpanById(spanId: string): Span | undefined
unregisterSpan(spanId: string): voidTypes
RisicareConfig
interface RisicareConfig {
apiKey?: string;
projectId?: string; // DEPRECATED — emits console.warn. Will be removed in v1.0.
endpoint?: string; // default 'https://ingest.risicare.ai' (spans)
apiEndpoint?: string; // default 'https://api.risicare.ai' (scores)
environment?: string;
serviceName?: string;
serviceVersion?: string;
enabled?: boolean;
traceContent?: boolean;
compress?: boolean;
sampleRate?: number;
batchSize?: number;
batchTimeoutMs?: number;
maxQueueSize?: number;
debug?: boolean;
fixRuntime?: boolean; // default false since 0.7.0
metadata?: Record<string, unknown>;
mask?: (key: string, value: unknown) => unknown;
}TraceContext
interface TraceContext {
traceId: string;
spanId: string;
sessionId?: string;
agentId?: string;
}Span
Span and Tracer are types only. Import them with
import type { Span, Tracer } from 'risicare'. A value import
(import { Span } from 'risicare') is a compile error in TypeScript (TS2693), and a
SyntaxError at run time in plain JavaScript under ESM (under CommonJS, require('risicare').Span is undefined).
class Span {
traceId: string;
spanId: string;
name: string;
kind: SpanKind;
startTime: string; // ISO 8601 timestamp
endTime?: string; // ISO 8601 timestamp, set on end()
attributes: Record<string, unknown>;
events: SpanEvent[];
status: SpanStatus;
setAttribute(key: string, value: unknown): this;
addEvent(name: string, attributes?: Record<string, unknown>): this;
setStatus(status: SpanStatus, message?: string): this;
end(): void;
}SpanKind
enum SpanKind {
INTERNAL = 'internal',
SERVER = 'server',
CLIENT = 'client',
PRODUCER = 'producer',
CONSUMER = 'consumer',
AGENT = 'agent',
LLM_CALL = 'llm_call',
TOOL_CALL = 'tool_call',
RETRIEVAL = 'retrieval',
DECISION = 'decision',
DELEGATION = 'delegation',
COORDINATION = 'coordination',
MESSAGE = 'message',
THINK = 'think',
DECIDE = 'decide',
OBSERVE = 'observe',
REFLECT = 'reflect',
}SpanStatus
enum SpanStatus {
UNSET = 'unset',
OK = 'ok',
ERROR = 'error',
}