Reference
Customization
Configuration, beforeSend, debug logging, error handling, and SDK exports.
Configuration reference
const otis = initOtis({
apiKey: "sk-otis-xxx", // Required (or OTIS_API_KEY env var on server)
serviceName: "my-app", // Required
disabled: false, // Silently drop all spans (default: false)
endpoint: "https://...", // Default: https://ingest.runotis.com
serverless: true, // Flush immediately (Lambda, Vercel, Workers)
beforeSend: (span) => span, // Pre-send hook for filtering / enrichment
identifierHashing: true, // HMAC-SHA256 hashing for all identifiers (default: true)
browser: { // Browser identity via cookies
autoAnonymousUserId: true,
autoSessionId: true,
},
debug: true, // Debug logging
piiRedaction: { // PII redaction (enabled by default)
enabled: true,
disabledPatterns: ["ipv4"],
},
});| Option | Env var | Default | Description |
|---|---|---|---|
apiKey | OTIS_API_KEY | required unless disabled | Authentication token |
serviceName | — | required unless disabled | Service name for span attribution |
endpoint | OTIS_ENDPOINT | https://ingest.runotis.com | Collector URL |
disabled | — | false | All spans silently dropped, no network requests |
serverless | — | false | Immediate flush for Lambda / Vercel / Workers |
beforeSend | — | — | Pre-send hook for filtering / enrichment |
debug | — | false | Debug logging |
identifierHashing | — | true | HMAC identifier pseudonymization with salt derived from API key (see Privacy) |
browser | — | — | Browser identity config |
piiRedaction | — | { enabled: true } | Client-side PII redaction (see Privacy) |
Browser note: OTIS_API_KEY and OTIS_ENDPOINT env vars are not available in browsers. Pass them explicitly. The browser initOtis always flushes immediately (equivalent to serverless: true).
beforeSend hook
Full control over every span before export. Runs after PII redaction, so the span you receive has already been scrubbed.
Return the span (optionally modified) to send it, or null to drop it.
const otis = initOtis({
apiKey: "sk-otis-xxx",
serviceName: "my-app",
beforeSend: (span) => {
// Drop noisy spans
if (span.name.startsWith("internal.healthcheck")) return null;
// Add required attributes to every span
span.attributes = {
...span.attributes,
"deploy.env": "production",
"app.version": "1.2.3",
};
return span;
},
});Filter to AI spans only
import { initOtis, isAISpan } from "@runotis/sdk";
const otis = initOtis({
serviceName: "my-app",
beforeSend: (span) => isAISpan(span) ? span : null,
});isAISpan() recognizes these prefixes:
| Prefix | Source |
|---|---|
ai.* | otis.wrap() spans (Vercel AI SDK) |
gen_ai.* | OpenAI, Anthropic direct SDK instrumentations |
llm.* | LangChain, LlamaIndex instrumentations |
otis.* | Otis-namespaced spans |
Event spans bypass beforeSend
Event spans bypass beforeSend
Spans produced by identifyUser, setUserProperties, setGroupProperties, sendEvent, and sendFeedbackSignal are always forwarded; they bypass beforeSend entirely. These carry user identity and feedback data that's needed regardless of filtering. You can safely return null for everything in beforeSend without losing identity or feedback data.
Debug logging
initOtis({ serviceName: "my-app", debug: true }); // all activity
initOtis({ serviceName: "my-app", debug: "traced,wrap" }); // specific types
initOtis({ serviceName: "my-app", debug: "verbose" }); // full payloads
initOtis({ serviceName: "my-app", debug: "verbose:filter" }); // verbose for specific types| Type | What it logs |
|---|---|
identifyUser | user ID, group count |
setUserProperties | user ID, property count |
setGroupProperties | group type, group ID, property count |
sendEvent | event name, attribute count |
sendFeedbackSignal | event ID, signal name |
traced | function name, arg count |
wrap | wrapped function name |
filter | span name, kept/dropped, reason |
export | batch size; verbose: span names |
Output format: console.debug("[otis:<type>]", summary, payload?)
Sample output when using beforeSend with debug: "filter":
[otis:filter] { span: 'ai.generateText', kept: true, reason: 'beforeSend' }
[otis:filter] { span: 'http.request', kept: false, reason: 'beforeSend_drop' }
[otis:filter] { span: 'otis_identifyUser', kept: true, reason: 'otis_event' }Error handling
SDK export errors
By default, errors during span export are logged to console.error. Register a listener for programmatic handling:
otis.on("error", (err) => {
myErrorReporter.captureException(err);
});Application errors
See sendException() for surfacing app errors as their own span.
Already using OpenTelemetry?
If your app already has OpenTelemetry instrumentation you want to keep, or you're emitting OTel spans from a non-wrapped AI library, see the OpenTelemetry integration guide. It covers OtisSpanProcessor, OtisExporter, the GenAI attribute conventions Otis recognizes, and how otis.wrap() coexists with an existing tracer provider.
Key types & exports
import {
// Initialization
initOtis,
getOtisInstance,
shutdownOtis,
// Core class
Otis,
// Wrapping
type WrapContext,
type WrapOptions,
type OtisOptions,
extractUserTextContent,
// Chat request helpers
contextFromChatRequest,
type ChatRequestContextOptions,
OTIS_SESSION_COOKIE, // "__otis_session"
OTIS_SESSION_HEADER, // "x-otis-session-id"
// Span
OtisSpan,
getSpanId,
// Event helpers
identifyUser,
setUserProperties,
setGroupProperties,
sendEvent,
sendFeedbackSignal,
// Context inspection
currentSpan,
withCurrent,
// Application errors
sendException,
// Per-call metadata
eventMetadata,
// Browser identity
CookieIdentityManager,
type BrowserIdentityConfig,
// Hashing
isHashedUserId,
isHashedSessionId,
isHashedGroupId,
// Filtering
isAISpan,
// PII redaction defaults
DEFAULT_SCAN_ATTRIBUTES,
DEFAULT_SCAN_ATTRIBUTE_PREFIXES,
// Escape hatch
OtisSpanProcessor,
OtisExporter,
} from "@runotis/sdk";
// Next.js client
import {
OtisProvider,
useOtis,
OtisPageView,
withOtisConfig,
} from "@runotis/sdk/next";
// Next.js server
import {
createOtisInstrumentation,
getServerOtis,
} from "@runotis/sdk/next/server";