Brokle JavaScript/TypeScript SDK Development Skill
Comprehensive guidance for developing the Brokle JavaScript/TypeScript SDK - an OTEL-native observability SDK for AI applications.
Overview
The Brokle JavaScript SDK is built on @opentelemetry/sdk-node with full GenAI 1.28+ semantic conventions compliance. It's a single package with sub-path exports providing multiple integration patterns.
Architecture: OTEL-native with TracerProvider → BrokleSpanProcessor → OTLP/HTTP Exporter (Protobuf+Gzip)
Single Package Structure with Sub-Path Exports
sdk/javascript/ ├── src/ │ ├── index.ts # Main exports (brokle) │ ├── client.ts # Brokle client │ ├── config.ts # Configuration │ ├── integrations/ │ │ ├── openai/ # brokle/openai │ │ ├── anthropic/ # brokle/anthropic │ │ └── langchain/ # brokle/langchain │ └── scorers/ # brokle/scorers ├── dist/ # Build output ├── examples/ │ ├── basic-usage.ts │ ├── wrapper-usage.ts │ └── langchain-integration.ts └── tests/
Usage: Tree-shakeable sub-path imports - users only import what they need.
Public API Surface
Core Package (brokle)
// Core client
export { Brokle, getClient, resetClient } from './client';
// Configuration
export type { BrokleConfig, BrokleConfigInput } from './types/config';
export { loadFromEnv, validateConfig } from './config';
// Decorators
export { observe, traceFunction } from './decorators';
export type { ObserveOptions } from './decorators';
// Type-safe attributes
export {
Attrs,
BrokleOtelSpanAttributes,
SpanType,
LLMProvider,
OperationType,
} from './types/attributes';
// Advanced use cases
export { createBrokleExporter } from './exporter';
export { BrokleSpanProcessor } from './processor';
OpenAI Wrapper (brokle/openai)
import { wrapOpenAI } from 'brokle/openai';
Anthropic Wrapper (brokle/anthropic)
import { wrapAnthropic } from 'brokle/anthropic';
LangChain Integration (brokle/langchain)
import { BrokleLangChainCallback } from 'brokle/langchain';
OTEL-Native Architecture
Architecture Flow
User Code ↓ startActiveSpan() / @observe / wrappers ↓ NodeTracerProvider (TraceIdRatioBasedSampler) ↓ BrokleSpanProcessor (wrapper pattern) ├→ BatchSpanProcessor (long-running apps) └→ SimpleSpanProcessor (serverless/Lambda) ↓ OTLPTraceExporter (Protobuf + Gzip) ↓ HTTP POST /v1/traces (OTLP standard) ↓ Brokle Backend
Key Components
TracerProvider Setup:
- •Resource with service name (respects
OTEL_SERVICE_NAME) - •TraceIdRatioBasedSampler (trace-level sampling)
- •Registered globally for OTEL ecosystem compatibility
BrokleSpanProcessor (Wrapper Pattern):
- •Wraps
BatchSpanProcessorORSimpleSpanProcessor - •Choice based on
flushSyncconfiguration - •Automatic environment/release attribute injection
- •Future: PII masking, attribute transformation
OTLP/HTTP Exporter:
- •Endpoint:
{base_url}/v1/traces(OpenTelemetry standard) - •Headers:
X-API-Key,X-Brokle-Environment,X-Brokle-Release - •Compression: Gzip (automatic, 65% bandwidth reduction)
- •Format: Protobuf
Symbol.for() Singleton:
- •Uses
Symbol.for('brokle')for global state - •Cross-realm safe (ESM, CJS, VM contexts)
- •No automatic process exit handlers (explicit lifecycle)
Five Integration Patterns
Pattern 1: @observe Decorator (TypeScript 5.0+)
Basic Usage:
import { observe } from 'brokle';
class AIService {
@observe()
async processRequest(input: string): Promise<string> {
return `Processed: ${input}`;
}
}
Full Options:
class AIService {
@observe({
name: 'ask-llm',
asType: 'generation', // span, generation, event, tool
userId: 'user-123',
sessionId: 'session-456',
tags: ['production', 'critical'],
metadata: { feature: 'chat' },
version: '1.0', // A/B testing support
captureInput: true,
captureOutput: true,
})
async askLLM(prompt: string): Promise<string> {
const response = await openai.chat.completions.create({...});
return response.choices[0].message.content;
}
}
TypeScript Requirements:
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"module": "ESNext"
}
}
Pattern 2: startActiveSpan() Callback Helper
Basic Usage:
import { getClient } from 'brokle';
const client = getClient();
const result = await client.startActiveSpan(
'my-operation',
async (span) => {
span.setAttribute('custom', 'value');
return await doWork();
}
);
With Version (A/B Testing):
const result = await client.startActiveSpan(
'my-operation',
async (span) => {
span.setAttribute(Attrs.USER_ID, 'user-123');
return await doWork();
},
undefined, // attributes
{ version: '1.0' } // A/B testing
);
Pattern 3: startActiveGeneration() Helper
LLM Generation Tracking:
import { getClient, Attrs } from 'brokle';
const client = getClient();
const response = await client.startActiveGeneration(
'chat',
'gpt-4',
'openai',
async (span) => {
const res = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
});
// Add GenAI 1.28+ attributes
span.setAttribute(Attrs.GEN_AI_OUTPUT_MESSAGES, JSON.stringify([...]));
span.setAttribute(Attrs.GEN_AI_USAGE_INPUT_TOKENS, res.usage.prompt_tokens);
span.setAttribute(Attrs.GEN_AI_USAGE_OUTPUT_TOKENS, res.usage.completion_tokens);
return res;
},
{ version: '1.0' } // A/B testing
);
Pattern 4: OpenAI Wrapper (Proxy Pattern)
Zero-Code Integration:
import { wrapOpenAI } from 'brokle/openai';
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const tracedOpenAI = wrapOpenAI(openai);
// All calls automatically tracked with GenAI 1.28+ attributes
const response = await tracedOpenAI.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
temperature: 0.7,
});
Wrapped Methods:
- •
openai.chat.completions.create→ Chat completions - •
openai.completions.create→ Text completions - •
openai.embeddings.create→ Embeddings
Automatic Attribute Extraction:
- •Provider:
openai - •Model: From request/response
- •Messages: Input and output messages
- •Tokens: Prompt, completion, total
- •Parameters: Temperature, max_tokens, top_p, etc.
Pattern 5: LangChain Integration
Callback Handler:
import { BrokleLangChainCallback } from 'brokle/langchain';
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { LLMChain } from 'langchain/chains';
const callback = new BrokleLangChainCallback();
const llm = new ChatOpenAI({ temperature: 0.7 });
const prompt = PromptTemplate.fromTemplate('Tell me about {topic}');
const chain = new LLMChain({ llm, prompt, callbacks: [callback] });
await chain.call({ topic: 'AI' });
Automatic Tracking:
- •LLM calls (
handleLLMStart,handleLLMEnd) - •Chain execution (
handleChainStart,handleChainEnd) - •Tool usage (
handleToolStart,handleToolEnd) - •Parent-child span relationships
Configuration
Two Configuration Patterns
Pattern 1: Explicit Configuration
import { Brokle } from 'brokle';
const client = new Brokle({
apiKey: 'bk_your_secret',
baseUrl: 'http://localhost:8080',
environment: 'production',
debug: true,
tracingEnabled: true,
release: 'v1.2.3',
sampleRate: 0.1, // Sample 10% of traces
flushSync: false, // BatchSpanProcessor (long-running)
flushAt: 200, // Batch size
flushInterval: 10, // Seconds
maxQueueSize: 10000,
timeout: 30000, // Milliseconds
});
Pattern 2: Environment-Based Singleton
import { getClient } from 'brokle';
// Reads from BROKLE_* environment variables
const client = getClient();
// All subsequent calls return same instance
const client2 = getClient(); // Same instance
Environment Variables
# Required BROKLE_API_KEY=bk_your_secret # Optional BROKLE_BASE_URL=http://localhost:8080 BROKLE_ENVIRONMENT=production BROKLE_RELEASE=v1.2.3 BROKLE_DEBUG=true BROKLE_TRACING_ENABLED=true BROKLE_SAMPLE_RATE=1.0 # 0.0 to 1.0 BROKLE_FLUSH_AT=100 # Batch size BROKLE_FLUSH_INTERVAL=10 # Seconds BROKLE_FLUSH_SYNC=false # true for serverless BROKLE_MAX_QUEUE_SIZE=10000 BROKLE_TIMEOUT=30000 # Milliseconds
Serverless Configuration
const client = new Brokle({
apiKey: process.env.BROKLE_API_KEY,
flushSync: true, // SimpleSpanProcessor for immediate export
});
Validation Rules
API Key:
- •Format:
bk_+ 40 alphanumeric characters (43 total) - •Example:
bk_1234567890abcdefghijklmnopqrstuvwxyz1234
Sample Rate:
- •Range: 0.0 to 1.0
- •Checks for NaN and Infinity
- •Trace-level sampling (entire traces sampled together)
Symbol.for() Singleton Pattern
Implementation
const BROKLE_GLOBAL_SYMBOL = Symbol.for('brokle');
interface BrokleGlobalState {
provider: NodeTracerProvider | null;
client: Brokle | null;
}
function getGlobalState(): BrokleGlobalState {
const g = globalThis as typeof globalThis & {
[BROKLE_GLOBAL_SYMBOL]?: BrokleGlobalState;
};
if (!g[BROKLE_GLOBAL_SYMBOL]) {
Object.defineProperty(g, BROKLE_GLOBAL_SYMBOL, {
value: { provider: null, client: null },
writable: false,
configurable: false,
enumerable: false,
});
}
return g[BROKLE_GLOBAL_SYMBOL]!;
}
export function getClient(config?: BrokleConfigInput): Brokle {
const state = getGlobalState();
if (!state.client) {
const clientConfig = config || loadFromEnv();
state.client = new Brokle(clientConfig);
state.provider = state.client.getProvider();
}
return state.client;
}
Why Symbol.for()?
- •Cross-Realm Uniqueness: Works across ESM, CJS, VM contexts
- •Cannot Be Overwritten: Symbol guarantees uniqueness
- •Global Registry:
Symbol.for()creates process-wide registry - •More Robust: Better than
global.__brokle__pattern
Lifecycle Management
No Automatic Exit Handlers
Important: SDK does NOT register process exit handlers to prevent memory leaks.
Long-Running Apps (BatchSpanProcessor):
const client = getClient({ flushSync: false });
// Explicit shutdown on SIGTERM
process.on('SIGTERM', async () => {
console.log('Shutting down...');
await client.shutdown(); // Flush + cleanup
process.exit(0);
});
Serverless/Lambda (SimpleSpanProcessor):
export const handler = async (event, context) => {
const client = getClient({ flushSync: true });
const result = await client.startActiveSpan('lambda-handler', async (span) => {
const res = await processEvent(event);
span.setAttribute('result', res);
return res;
});
// CRITICAL: Flush before exit
await client.flush();
return result;
};
Flush vs Shutdown
flush(): Force export pending spans
await client.flush();
shutdown(): Flush + cleanup resources
await client.shutdown();
GenAI 1.28+ Attributes (Type-Safe)
Provider & Operation
import { Attrs, LLMProvider } from 'brokle';
span.setAttribute(Attrs.GEN_AI_PROVIDER_NAME, LLMProvider.OPENAI);
span.setAttribute(Attrs.GEN_AI_OPERATION_NAME, 'chat');
Request Parameters
span.setAttribute(Attrs.GEN_AI_REQUEST_MODEL, 'gpt-4'); span.setAttribute(Attrs.GEN_AI_REQUEST_TEMPERATURE, 0.7); span.setAttribute(Attrs.GEN_AI_REQUEST_MAX_TOKENS, 100); span.setAttribute(Attrs.GEN_AI_REQUEST_TOP_P, 1.0);
Messages (JSON Format)
// Input messages
span.setAttribute(
Attrs.GEN_AI_INPUT_MESSAGES,
JSON.stringify([{ role: 'user', content: 'Hello' }])
);
// Output messages
span.setAttribute(
Attrs.GEN_AI_OUTPUT_MESSAGES,
JSON.stringify([{ role: 'assistant', content: 'Hi there!' }])
);
Usage Metrics
span.setAttribute(Attrs.GEN_AI_USAGE_INPUT_TOKENS, 10); span.setAttribute(Attrs.GEN_AI_USAGE_OUTPUT_TOKENS, 20);
Brokle Custom Attributes
// Span type
span.setAttribute(Attrs.BROKLE_SPAN_TYPE, 'generation');
// A/B testing
span.setAttribute(Attrs.BROKLE_VERSION, '1.0');
// Filterable metadata
span.setAttribute(Attrs.USER_ID, 'user-123');
span.setAttribute(Attrs.SESSION_ID, 'session-456');
span.setAttribute(Attrs.BROKLE_TRACE_TAGS, JSON.stringify(['production']));
span.setAttribute(Attrs.BROKLE_TRACE_METADATA, JSON.stringify({ feature: 'chat' }));
Development Commands
Installation
# Install dependencies pnpm install
Build
# Build the package pnpm build # Watch mode pnpm dev
Testing
# Run all tests pnpm test # Watch mode pnpm test:watch
Code Quality
# Lint pnpm lint # Type checking pnpm typecheck # Format code pnpm format # Format check pnpm format:check
Clean
# Remove build artifacts and node_modules pnpm clean
Build Configuration (tsup)
Dual Build (ESM + CJS)
// tsup.config.ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'], // Dual build
dts: true, // Generate .d.ts
sourcemap: true,
clean: true,
treeshake: true,
splitting: false,
minify: false,
outDir: 'dist',
target: 'node20',
platform: 'node',
});
Output:
- •
dist/index.js(CommonJS) - •
dist/index.mjs(ES Modules) - •
dist/index.d.ts(TypeScript types)
Testing Patterns
Unit Tests
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getClient, resetClient } from 'brokle';
describe('Brokle Client', () => {
afterEach(async () => {
await resetClient();
});
it('should create singleton instance', () => {
const client1 = getClient();
const client2 = getClient();
expect(client1).toBe(client2);
});
it('should validate API key format', () => {
expect(() => {
new Brokle({ apiKey: 'invalid' });
}).toThrow('must start with "bk_"');
});
});
Mock Spans
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('test');
const span = tracer.startSpan('test-span');
span.setAttribute('test', 'value');
span.end();
Common Patterns
Nested Spans
const tracer = client.getTracer();
await tracer.startActiveSpan('parent', async (parent) => {
parent.setAttribute('parent_data', 'value');
// Child span
await tracer.startActiveSpan('child', async (child) => {
child.setAttribute('child_data', 'value');
child.end();
});
parent.end();
});
traceFunction Helper
import { traceFunction } from 'brokle';
const tracedFn = traceFunction(
'process-data',
async (data: string) => {
return processData(data);
},
{ captureInput: true, version: '1.0' }
);
const result = await tracedFn('hello');
Key Architectural Decisions
- •Symbol.for() Singleton: Cross-realm safe, prevents accidental overwrites
- •No Exit Handlers: Explicit lifecycle management to prevent memory leaks
- •Wrapper Pattern: Processor wraps Batch/Simple for flexibility
- •Proxy Pattern: OpenAI/Anthropic wrappers use Proxy for zero-code integration
- •Dual Build: ESM + CJS for maximum compatibility
- •TypeScript First: Full type safety with GenAI 1.28+ attributes
Troubleshooting
experimentalDecorators Required
Problem: Decorator errors in TypeScript
Solution: Add to tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Symbol.for() Singleton Issues
Problem: Multiple client instances created
Solution: Symbol.for() creates process-wide registry. Check if using new Brokle() instead of getClient().
flushSync for Serverless
Problem: Spans not appearing in Lambda
Solution: Use flushSync: true for SimpleSpanProcessor:
const client = getClient({ flushSync: true });
await client.flush(); // Before exit
No Automatic Cleanup
Problem: Spans not exported on exit
Solution: SDK doesn't register exit handlers. Call client.flush() or client.shutdown() explicitly.
Key Differences from Python SDK
| Feature | JavaScript | Python |
|---|---|---|
| Singleton | Symbol.for() | Global dict + atexit |
| Lifecycle | Explicit (no auto handlers) | Automatic atexit cleanup |
| Processors | Batch OR Simple (via flushSync) | Always Batch |
| Decorators | TypeScript decorators | Python decorators |
| Wrappers | Proxy pattern (sub-path exports) | Wrapper functions (in core) |
| Build | ESM + CJS dual build | Python package |
| Structure | Single package with sub-path exports | Single package |
Reference
- •SDK Location:
sdk/javascript/ - •Source Code:
src/ - •Integrations:
src/integrations/(openai, anthropic, langchain) - •Documentation:
README.md,CLAUDE.md - •Examples:
examples/directory - •Tests:
src/**/*.test.ts(unit),tests/(e2e)