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 completionsopenai.completions.create→ Text completionsopenai.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)
Converted and distributed by TomeVault — claim your Tome and manage your conversions.