or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

active-observations.mdattribute-creation.mdcontext-management.mdindex.mdmanual-observations.mdobservation-types.mdobserve-decorator.mdotel-span-attributes.mdtrace-id-generation.mdtracer-provider.md

observe-decorator.mddocs/

0

# Observe Decorator

1

2

The `observe()` function provides a decorator-style approach to adding observability to existing functions without modifying their implementation. It wraps functions with automatic tracing, input/output capture, and error tracking while preserving their original behavior and type signatures.

3

4

## Core Function

5

6

### observe

7

8

Decorator function that automatically wraps any function with Langfuse observability.

9

10

```typescript { .api }

11

/**

12

* Decorator function that automatically wraps any function with Langfuse observability.

13

*

14

* @param fn - The function to wrap with observability

15

* @param options - Configuration for observation behavior

16

* @returns An instrumented version of the function

17

*/

18

function observe<T extends (...args: any[]) => any>(

19

fn: T,

20

options?: ObserveOptions

21

): T;

22

23

interface ObserveOptions {

24

/** Name for the observation (defaults to function name) */

25

name?: string;

26

/** Type of observation to create */

27

asType?: LangfuseObservationType;

28

/** Whether to capture function input as observation input. Default: true */

29

captureInput?: boolean;

30

/** Whether to capture function output as observation output. Default: true */

31

captureOutput?: boolean;

32

/** Parent span context to attach this observation to */

33

parentSpanContext?: SpanContext;

34

/** Whether to automatically end the observation when exiting. Default: true */

35

endOnExit?: boolean;

36

}

37

```

38

39

## Key Features

40

41

### Zero Code Changes

42

43

Wrap existing functions without modifying their internal logic.

44

45

```typescript

46

import { observe } from '@langfuse/tracing';

47

48

// Original function

49

async function fetchUserData(userId: string) {

50

const response = await fetch(`/api/users/${userId}`);

51

return response.json();

52

}

53

54

// Wrapped function - behavior unchanged, now with observability

55

const tracedFetchUserData = observe(fetchUserData, {

56

name: 'fetch-user-data',

57

asType: 'span'

58

});

59

60

// Use exactly as before

61

const user = await tracedFetchUserData('user-123');

62

```

63

64

### Automatic I/O Capture

65

66

Function arguments and return values are automatically captured as input and output.

67

68

```typescript

69

const processOrder = observe(

70

async (orderId: string, items: CartItem[]) => {

71

const validation = await validateOrder(orderId, items);

72

const payment = await processPayment(validation);

73

const shipping = await scheduleShipping(payment);

74

75

return {

76

orderId,

77

status: 'confirmed',

78

trackingId: shipping.id

79

};

80

},

81

{

82

name: 'process-order',

83

captureInput: true, // Captures [orderId, items]

84

captureOutput: true // Captures return value

85

}

86

);

87

88

// Input and output automatically logged

89

const result = await processOrder('ord_123', cartItems);

90

```

91

92

### Error Tracking

93

94

Errors are automatically captured with error level and status message.

95

96

```typescript

97

const riskyOperation = observe(

98

async (data: string) => {

99

if (!data) {

100

throw new Error('Data is required');

101

}

102

return processData(data);

103

},

104

{ name: 'risky-operation' }

105

);

106

107

try {

108

await riskyOperation('');

109

} catch (error) {

110

// Error automatically captured in observation with level: 'ERROR'

111

}

112

```

113

114

### Type Preservation

115

116

The wrapped function maintains the original signature and return types.

117

118

```typescript

119

// Original function with specific types

120

async function calculateTotal(

121

items: Item[],

122

taxRate: number

123

): Promise<{ subtotal: number; tax: number; total: number }> {

124

const subtotal = items.reduce((sum, item) => sum + item.price, 0);

125

const tax = subtotal * taxRate;

126

return { subtotal, tax, total: subtotal + tax };

127

}

128

129

// Wrapped function maintains exact types

130

const tracedCalculateTotal = observe(calculateTotal, {

131

name: 'calculate-total'

132

});

133

134

// TypeScript knows the return type

135

const result: Promise<{ subtotal: number; tax: number; total: number }> =

136

tracedCalculateTotal(items, 0.08);

137

```

138

139

## Basic Usage

140

141

### Simple Function Wrapping

142

143

```typescript

144

import { observe } from '@langfuse/tracing';

145

146

// Wrap a synchronous function

147

const addNumbers = observe(

148

(a: number, b: number) => a + b,

149

{ name: 'add-numbers' }

150

);

151

152

const sum = addNumbers(5, 3); // 8

153

154

// Wrap an async function

155

const fetchData = observe(

156

async (url: string) => {

157

const response = await fetch(url);

158

return response.json();

159

},

160

{ name: 'fetch-data' }

161

);

162

163

const data = await fetchData('https://api.example.com/data');

164

```

165

166

### Named vs Anonymous Functions

167

168

The decorator uses the function name by default, but you can override it.

169

170

```typescript

171

// Named function - uses function name as observation name

172

function processUser(userId: string) {

173

return { id: userId, processed: true };

174

}

175

176

const traced = observe(processUser); // name: 'processUser'

177

178

// Anonymous function - provide explicit name

179

const tracedAnon = observe(

180

(userId: string) => ({ id: userId, processed: true }),

181

{ name: 'process-user-anonymous' }

182

);

183

```

184

185

## LLM Function Wrapping

186

187

### Generation Observation

188

189

Wrap LLM API calls with generation-type observations.

190

191

```typescript

192

const generateSummary = observe(

193

async (document: string, maxWords: number = 100) => {

194

const response = await openai.chat.completions.create({

195

model: 'gpt-4-turbo',

196

messages: [

197

{ role: 'system', content: `Summarize in ${maxWords} words or less` },

198

{ role: 'user', content: document }

199

],

200

max_tokens: maxWords * 2

201

});

202

203

return response.choices[0].message.content;

204

},

205

{

206

name: 'document-summarizer',

207

asType: 'generation',

208

captureInput: true,

209

captureOutput: true

210

}

211

);

212

213

// Use the wrapped function

214

const summary = await generateSummary(longDocument, 150);

215

```

216

217

### Embedding Generation

218

219

```typescript

220

const generateEmbeddings = observe(

221

async (texts: string[]) => {

222

const response = await openai.embeddings.create({

223

model: 'text-embedding-ada-002',

224

input: texts

225

});

226

227

return response.data.map(item => item.embedding);

228

},

229

{

230

name: 'text-embedder',

231

asType: 'embedding',

232

captureInput: true,

233

captureOutput: false // Don't log large vectors

234

}

235

);

236

237

const vectors = await generateEmbeddings(['Hello', 'World']);

238

```

239

240

## Specialized Observation Types

241

242

### Agent Function

243

244

```typescript

245

const researchAgent = observe(

246

async (query: string, maxSources: number = 3) => {

247

// Search for relevant documents

248

const documents = await searchDocuments(query, maxSources * 2);

249

250

// Filter and rank results

251

const topDocs = documents

252

.filter(d => d.score > 0.7)

253

.slice(0, maxSources);

254

255

// Generate comprehensive answer

256

const context = topDocs.map(d => d.content).join('\n\n');

257

const answer = await generateSummary(

258

`Based on: ${context}\n\nQuestion: ${query}`,

259

200

260

);

261

262

return {

263

answer,

264

sources: topDocs.map(d => d.source),

265

confidence: Math.min(...topDocs.map(d => d.score))

266

};

267

},

268

{

269

name: 'research-agent',

270

asType: 'agent',

271

captureInput: true,

272

captureOutput: true

273

}

274

);

275

```

276

277

### Tool Function

278

279

```typescript

280

const searchDocuments = observe(

281

async (query: string, topK: number = 5) => {

282

const embedding = await embedText(query);

283

const results = await vectorDb.search(embedding, topK);

284

285

return results.map(r => ({

286

content: r.metadata.content,

287

score: r.score,

288

source: r.metadata.source

289

}));

290

},

291

{

292

name: 'document-search',

293

asType: 'retriever',

294

captureInput: true,

295

captureOutput: true

296

}

297

);

298

```

299

300

### Evaluator Function

301

302

```typescript

303

const evaluateResponse = observe(

304

(response: string, reference: string, metric: string = 'similarity') => {

305

let score: number;

306

307

switch (metric) {

308

case 'similarity':

309

score = calculateCosineSimilarity(response, reference);

310

break;

311

case 'bleu':

312

score = calculateBleuScore(response, reference);

313

break;

314

default:

315

throw new Error(`Unknown metric: ${metric}`);

316

}

317

318

return {

319

score,

320

passed: score > 0.8,

321

metric,

322

grade: score > 0.9 ? 'excellent' : score > 0.7 ? 'good' : 'needs_improvement'

323

};

324

},

325

{

326

name: 'response-evaluator',

327

asType: 'evaluator',

328

captureInput: true,

329

captureOutput: true

330

}

331

);

332

```

333

334

### Guardrail Function

335

336

```typescript

337

const moderateContent = observe(

338

async (text: string, policies: string[] = ['profanity', 'spam']) => {

339

const violations = [];

340

341

for (const policy of policies) {

342

const result = await checkPolicy(text, policy);

343

if (result.violation) {

344

violations.push({ policy, severity: result.severity });

345

}

346

}

347

348

return {

349

allowed: violations.length === 0,

350

violations,

351

confidence: 0.95

352

};

353

},

354

{

355

name: 'content-moderator',

356

asType: 'guardrail',

357

captureInput: true,

358

captureOutput: true

359

}

360

);

361

```

362

363

## Class Method Decoration

364

365

### Constructor Pattern

366

367

Wrap methods during class instantiation.

368

369

```typescript

370

class UserService {

371

private db: Database;

372

373

constructor(database: Database) {

374

this.db = database;

375

376

// Wrap methods in constructor

377

this.createUser = observe(this.createUser.bind(this), {

378

name: 'create-user',

379

asType: 'span',

380

captureInput: false, // Sensitive data

381

captureOutput: true

382

});

383

384

this.fetchUser = observe(this.fetchUser.bind(this), {

385

name: 'fetch-user',

386

asType: 'span'

387

});

388

}

389

390

async createUser(userData: UserData) {

391

// Implementation automatically traced

392

return await this.db.users.create(userData);

393

}

394

395

async fetchUser(userId: string) {

396

// Implementation automatically traced

397

return await this.db.users.findUnique({ where: { id: userId } });

398

}

399

}

400

401

const service = new UserService(db);

402

const user = await service.createUser({ name: 'Alice' });

403

```

404

405

### Factory Pattern

406

407

Create traced instances using a factory function.

408

409

```typescript

410

class AIService {

411

async generateText(prompt: string) {

412

return await llm.generate(prompt);

413

}

414

415

async embedText(text: string) {

416

return await embedder.embed(text);

417

}

418

}

419

420

function createTracedAIService(): AIService {

421

const service = new AIService();

422

423

service.generateText = observe(service.generateText.bind(service), {

424

name: 'ai-generate-text',

425

asType: 'generation'

426

});

427

428

service.embedText = observe(service.embedText.bind(service), {

429

name: 'ai-embed-text',

430

asType: 'embedding'

431

});

432

433

return service;

434

}

435

436

const ai = createTracedAIService();

437

```

438

439

## Input/Output Control

440

441

### Selective Capture

442

443

Control what gets captured to avoid logging sensitive data or large payloads.

444

445

```typescript

446

// Capture input but not output (sensitive results)

447

const fetchUserProfile = observe(

448

async (userId: string) => {

449

const user = await db.users.findUnique({ where: { id: userId } });

450

const preferences = await db.preferences.findMany({ where: { userId } });

451

return { ...user, preferences };

452

},

453

{

454

name: 'fetch-user-profile',

455

captureInput: false, // Don't capture user IDs

456

captureOutput: false // Don't capture sensitive profile data

457

}

458

);

459

460

// Capture output but not input (large payloads)

461

const processLargeFile = observe(

462

async (fileBuffer: Buffer) => {

463

const processed = await processFile(fileBuffer);

464

return { size: processed.length, checksum: processed.checksum };

465

},

466

{

467

name: 'process-file',

468

captureInput: false, // Don't log large buffer

469

captureOutput: true // Log summary info

470

}

471

);

472

```

473

474

### Argument Capture Behavior

475

476

Input capture handles different argument patterns automatically.

477

478

```typescript

479

// Single argument - captured as-is

480

const singleArg = observe(

481

(value: string) => value.toUpperCase(),

482

{ name: 'single-arg', captureInput: true }

483

);

484

singleArg('hello'); // input: "hello"

485

486

// Multiple arguments - captured as array

487

const multiArg = observe(

488

(a: number, b: number, c: number) => a + b + c,

489

{ name: 'multi-arg', captureInput: true }

490

);

491

multiArg(1, 2, 3); // input: [1, 2, 3]

492

493

// No arguments - input is undefined

494

const noArg = observe(

495

() => Date.now(),

496

{ name: 'no-arg', captureInput: true }

497

);

498

noArg(); // input: undefined

499

```

500

501

## Function Composition

502

503

Observed functions remain fully composable.

504

505

```typescript

506

// Individual observed functions

507

const fetchData = observe(

508

async (url: string) => {

509

const response = await fetch(url);

510

return response.json();

511

},

512

{ name: 'fetch-data', asType: 'tool' }

513

);

514

515

const processData = observe(

516

async (data: any) => {

517

return data.items.map(item => ({

518

id: item.id,

519

value: item.value * 2

520

}));

521

},

522

{ name: 'process-data', asType: 'span' }

523

);

524

525

const saveData = observe(

526

async (data: any) => {

527

return await db.items.createMany({ data });

528

},

529

{ name: 'save-data', asType: 'span' }

530

);

531

532

// Compose into pipeline

533

const dataPipeline = observe(

534

async (url: string) => {

535

const raw = await fetchData(url);

536

const processed = await processData(raw);

537

const saved = await saveData(processed);

538

return saved;

539

},

540

{ name: 'data-pipeline', asType: 'chain' }

541

);

542

543

// Single call creates hierarchical trace

544

await dataPipeline('https://api.example.com/data');

545

```

546

547

## Advanced Patterns

548

549

### Conditional Tracing

550

551

Wrap functions conditionally based on environment.

552

553

```typescript

554

function maybeObserve<T extends (...args: any[]) => any>(

555

fn: T,

556

options: ObserveOptions

557

): T {

558

if (process.env.LANGFUSE_ENABLED === 'true') {

559

return observe(fn, options);

560

}

561

return fn;

562

}

563

564

const processOrder = maybeObserve(

565

async (orderId: string) => {

566

return await performProcessing(orderId);

567

},

568

{ name: 'process-order' }

569

);

570

```

571

572

### Middleware Pattern

573

574

Create reusable observation middleware.

575

576

```typescript

577

function withObservation<T extends (...args: any[]) => any>(

578

options: ObserveOptions

579

) {

580

return (fn: T): T => {

581

return observe(fn, options);

582

};

583

}

584

585

// Create middleware

586

const asGeneration = withObservation({ asType: 'generation' });

587

const asTool = withObservation({ asType: 'tool' });

588

589

// Apply to functions

590

const generateText = asGeneration(async (prompt: string) => {

591

return await llm.generate(prompt);

592

});

593

594

const searchWeb = asTool(async (query: string) => {

595

return await webApi.search(query);

596

});

597

```

598

599

### Decorator Factory

600

601

Create domain-specific decorators.

602

603

```typescript

604

function observeLLM(name: string, model: string) {

605

return <T extends (...args: any[]) => any>(fn: T): T => {

606

return observe(fn, {

607

name,

608

asType: 'generation',

609

captureInput: true,

610

captureOutput: true

611

});

612

};

613

}

614

615

function observeTool(name: string) {

616

return <T extends (...args: any[]) => any>(fn: T): T => {

617

return observe(fn, {

618

name,

619

asType: 'tool',

620

captureInput: true,

621

captureOutput: true

622

});

623

};

624

}

625

626

// Use decorators

627

const chatGPT = observeLLM('chat-gpt', 'gpt-4')(

628

async (prompt: string) => await openai.chat(prompt)

629

);

630

631

const webSearch = observeTool('web-search')(

632

async (query: string) => await google.search(query)

633

);

634

```

635

636

## Error Handling

637

638

### Automatic Error Capture

639

640

Errors are automatically logged with error level.

641

642

```typescript

643

const riskyOperation = observe(

644

async (data: string) => {

645

if (!data) {

646

throw new Error('Data required');

647

}

648

649

const processed = await processData(data);

650

651

if (!processed.valid) {

652

throw new Error('Processing failed validation');

653

}

654

655

return processed;

656

},

657

{ name: 'risky-operation' }

658

);

659

660

try {

661

await riskyOperation('');

662

} catch (error) {

663

// Observation automatically updated with:

664

// - level: 'ERROR'

665

// - statusMessage: error.message

666

// - output: { error: error.message }

667

}

668

```

669

670

### Error Propagation

671

672

Errors are re-thrown after being captured, preserving error handling.

673

674

```typescript

675

const operation = observe(

676

async () => {

677

throw new Error('Operation failed');

678

},

679

{ name: 'failing-operation' }

680

);

681

682

try {

683

await operation();

684

} catch (error) {

685

// Error was logged in observation

686

console.error('Caught error:', error.message);

687

// Normal error handling continues

688

}

689

```

690

691

## Best Practices

692

693

### Descriptive Names

694

695

Use clear, descriptive names that indicate the operation's purpose.

696

697

```typescript

698

// Good: Specific and clear

699

const generateProductDescription = observe(fn, {

700

name: 'generate-product-description'

701

});

702

703

// Avoid: Generic and unclear

704

const process = observe(fn, { name: 'process' });

705

```

706

707

### Appropriate Observation Types

708

709

Choose the correct observation type for the operation.

710

711

```typescript

712

// LLM calls -> generation

713

const llmCall = observe(fn, { asType: 'generation' });

714

715

// API calls -> tool

716

const apiCall = observe(fn, { asType: 'tool' });

717

718

// Multi-step workflows -> chain

719

const pipeline = observe(fn, { asType: 'chain' });

720

721

// General operations -> span (default)

722

const operation = observe(fn, { asType: 'span' });

723

```

724

725

### Sensitive Data

726

727

Disable capture for functions handling sensitive information.

728

729

```typescript

730

const processPayment = observe(

731

async (cardNumber: string, cvv: string) => {

732

return await paymentGateway.charge({ cardNumber, cvv });

733

},

734

{

735

name: 'process-payment',

736

captureInput: false, // Don't log card details

737

captureOutput: false // Don't log payment response

738

}

739

);

740

```

741

742

### Performance Considerations

743

744

For high-frequency operations, consider disabling capture.

745

746

```typescript

747

const logMetric = observe(

748

(metric: string, value: number) => {

749

metrics.record(metric, value);

750

},

751

{

752

name: 'log-metric',

753

captureInput: false, // Reduce overhead

754

captureOutput: false

755

}

756

);

757

```

758

759

### Composition Over Deep Nesting

760

761

Prefer composing observed functions over deeply nested callbacks.

762

763

```typescript

764

// Good: Composed, traceable functions

765

const step1 = observe(async (data) => { /* ... */ }, { name: 'step-1' });

766

const step2 = observe(async (data) => { /* ... */ }, { name: 'step-2' });

767

const step3 = observe(async (data) => { /* ... */ }, { name: 'step-3' });

768

769

const pipeline = observe(

770

async (input) => {

771

const a = await step1(input);

772

const b = await step2(a);

773

return await step3(b);

774

},

775

{ name: 'pipeline', asType: 'chain' }

776

);

777

778

// Avoid: Deep nesting in single function

779

const monolithic = observe(

780

async (input) => {

781

const a = await /* complex step 1 */;

782

const b = await /* complex step 2 */;

783

return await /* complex step 3 */;

784

},

785

{ name: 'monolithic' }

786

);

787

```

788