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

active-observations.mddocs/

0

# Active Observations

1

2

Active observations with `startActiveObservation()` provide automatic lifecycle management for function-scoped operations. This approach handles observation creation, context activation, and cleanup automatically, making it ideal for wrapping asynchronous operations and ensuring proper error handling.

3

4

## Core Function

5

6

### startActiveObservation

7

8

Starts an observation and executes a function within its context with automatic lifecycle management.

9

10

```typescript { .api }

11

/**

12

* Starts an active observation and executes a function within its context.

13

*

14

* @param name - Descriptive name for the observation

15

* @param fn - Function to execute (receives typed observation instance)

16

* @param options - Configuration including observation type

17

* @returns The exact return value of the executed function

18

*/

19

function startActiveObservation<F extends (observation: LangfuseObservation) => unknown>(

20

name: string,

21

fn: F,

22

options?: StartActiveObservationOpts

23

): ReturnType<F>;

24

25

// Type-specific overloads

26

function startActiveObservation<F extends (generation: LangfuseGeneration) => unknown>(

27

name: string,

28

fn: F,

29

options: { asType: "generation" }

30

): ReturnType<F>;

31

32

function startActiveObservation<F extends (agent: LangfuseAgent) => unknown>(

33

name: string,

34

fn: F,

35

options: { asType: "agent" }

36

): ReturnType<F>;

37

38

// ... additional overloads for tool, chain, retriever, evaluator, guardrail, embedding

39

40

interface StartActiveObservationOpts {

41

/** Type of observation to create. Defaults to 'span' */

42

asType?: LangfuseObservationType;

43

/** Custom start time for the observation */

44

startTime?: Date;

45

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

46

parentSpanContext?: SpanContext;

47

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

48

endOnExit?: boolean;

49

}

50

```

51

52

## Key Features

53

54

### Automatic Context Management

55

56

The observation is automatically set as the active span in the OpenTelemetry context, making it the parent for any child observations created within the function.

57

58

```typescript

59

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

60

61

await startActiveObservation('parent-operation', async (observation) => {

62

observation.update({ input: { step: 'start' } });

63

64

// This child observation automatically inherits the parent context

65

await startActiveObservation('child-operation', async (child) => {

66

child.update({ input: { substep: 'processing' } });

67

// Process...

68

child.update({ output: { result: 'done' } });

69

});

70

71

observation.update({ output: { status: 'complete' } });

72

});

73

// Parent and child are automatically ended

74

```

75

76

### Lifecycle Automation

77

78

Observations are automatically ended when the function completes, whether it succeeds or throws an error.

79

80

```typescript

81

// Automatic ending on success

82

const result = await startActiveObservation(

83

'successful-operation',

84

async (observation) => {

85

observation.update({ input: { data: 'value' } });

86

const result = await processData();

87

observation.update({ output: result });

88

return result;

89

}

90

);

91

// Observation automatically ended with success status

92

93

// Automatic ending on error

94

try {

95

await startActiveObservation('failing-operation', async (observation) => {

96

observation.update({ input: { data: 'value' } });

97

throw new Error('Operation failed');

98

});

99

} catch (error) {

100

// Observation automatically ended with error status

101

}

102

```

103

104

### Type Safety

105

106

The function parameter is strongly typed based on the `asType` option, providing full IntelliSense support.

107

108

```typescript

109

// TypeScript knows 'generation' is LangfuseGeneration

110

await startActiveObservation(

111

'llm-call',

112

async (generation) => {

113

// generation.update accepts LangfuseGenerationAttributes

114

generation.update({

115

model: 'gpt-4',

116

modelParameters: { temperature: 0.7 },

117

usageDetails: { totalTokens: 500 }

118

});

119

},

120

{ asType: 'generation' }

121

);

122

```

123

124

## Synchronous Functions

125

126

Active observations work seamlessly with both sync and async functions.

127

128

```typescript

129

// Synchronous function

130

const result = startActiveObservation('sync-calculation', (observation) => {

131

observation.update({ input: { x: 10, y: 20 } });

132

133

const result = 10 + 20;

134

135

observation.update({ output: { result } });

136

return result;

137

});

138

139

console.log(result); // 30

140

```

141

142

## Asynchronous Functions

143

144

The return type preserves the Promise wrapper for async functions.

145

146

```typescript

147

// Asynchronous function

148

const result = await startActiveObservation(

149

'async-operation',

150

async (observation) => {

151

observation.update({ input: { userId: '123' } });

152

153

// Await internal operations

154

const data = await fetchUserData('123');

155

const processed = await processData(data);

156

157

observation.update({ output: { processed } });

158

return processed;

159

}

160

);

161

162

console.log(result); // Processed data

163

```

164

165

## Error Handling

166

167

Errors are automatically captured and the observation is marked with error status.

168

169

```typescript

170

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

171

172

try {

173

await startActiveObservation(

174

'risky-operation',

175

async (observation) => {

176

observation.update({

177

input: { operation: 'process-payment' }

178

});

179

180

try {

181

const result = await processPayment();

182

observation.update({ output: result });

183

return result;

184

} catch (error) {

185

// Manually capture error details

186

observation.update({

187

level: 'ERROR',

188

statusMessage: error.message,

189

output: { error: error.message, code: error.code }

190

});

191

throw error; // Re-throw to propagate

192

}

193

}

194

);

195

} catch (error) {

196

// Error was captured in observation and is now available here

197

console.error('Operation failed:', error);

198

}

199

```

200

201

## Nested Operations

202

203

Child observations created within the function automatically inherit the context.

204

205

```typescript

206

// RAG Chain with nested operations

207

const answer = await startActiveObservation(

208

'rag-qa-chain',

209

async (chain) => {

210

chain.update({

211

input: { question: 'How does photosynthesis work?' },

212

metadata: { vectorDb: 'pinecone', model: 'gpt-4' }

213

});

214

215

// Retrieval step - inherits chain context

216

const docs = await startActiveObservation(

217

'vector-retrieval',

218

async (retriever) => {

219

retriever.update({

220

input: { query: 'photosynthesis mechanism', topK: 5 }

221

});

222

223

const results = await vectorSearch('photosynthesis mechanism');

224

225

retriever.update({

226

output: { documents: results, count: results.length }

227

});

228

229

return results;

230

},

231

{ asType: 'retriever' }

232

);

233

234

// Generation step - also inherits chain context

235

const response = await startActiveObservation(

236

'answer-generation',

237

async (generation) => {

238

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

239

240

generation.update({

241

input: { question: 'How does photosynthesis work?', context },

242

model: 'gpt-4'

243

});

244

245

const answer = await generateAnswer(context);

246

247

generation.update({

248

output: { answer },

249

usageDetails: { totalTokens: 450 }

250

});

251

252

return answer;

253

},

254

{ asType: 'generation' }

255

);

256

257

chain.update({

258

output: { answer: response, sources: docs.length }

259

});

260

261

return response;

262

},

263

{ asType: 'chain' }

264

);

265

```

266

267

## Examples by Observation Type

268

269

### Span

270

271

Default observation type for general operations.

272

273

```typescript

274

const result = await startActiveObservation(

275

'data-processing',

276

async (span) => {

277

span.update({

278

input: { records: 1000 },

279

metadata: { version: '2.1.0' }

280

});

281

282

const processed = await processRecords(records);

283

284

span.update({

285

output: { processedCount: processed.length },

286

metadata: { duration: Date.now() - startTime }

287

});

288

289

return processed;

290

}

291

);

292

```

293

294

### Generation

295

296

For LLM calls and AI model interactions.

297

298

```typescript

299

const response = await startActiveObservation(

300

'openai-completion',

301

async (generation) => {

302

generation.update({

303

input: [

304

{ role: 'system', content: 'You are a helpful assistant' },

305

{ role: 'user', content: 'Explain AI ethics' }

306

],

307

model: 'gpt-4-turbo',

308

modelParameters: { temperature: 0.7, maxTokens: 500 }

309

});

310

311

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

312

model: 'gpt-4-turbo',

313

messages: [

314

{ role: 'system', content: 'You are a helpful assistant' },

315

{ role: 'user', content: 'Explain AI ethics' }

316

],

317

temperature: 0.7,

318

max_tokens: 500

319

});

320

321

generation.update({

322

output: result.choices[0].message,

323

usageDetails: {

324

promptTokens: result.usage.prompt_tokens,

325

completionTokens: result.usage.completion_tokens,

326

totalTokens: result.usage.total_tokens

327

},

328

costDetails: { totalCost: 0.002, currency: 'USD' }

329

});

330

331

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

332

},

333

{ asType: 'generation' }

334

);

335

```

336

337

### Agent

338

339

For AI agent workflows with tool usage.

340

341

```typescript

342

const agentResult = await startActiveObservation(

343

'research-agent',

344

async (agent) => {

345

agent.update({

346

input: { query: 'Latest climate change research' },

347

metadata: { tools: ['web-search', 'arxiv-search'], model: 'gpt-4' }

348

});

349

350

// Tool calls inherit the agent context automatically

351

const webResults = await startActiveObservation(

352

'web-search-tool',

353

async (tool) => {

354

tool.update({ input: { query: 'climate change 2024' } });

355

const results = await searchWeb('climate change 2024');

356

tool.update({ output: results });

357

return results;

358

},

359

{ asType: 'tool' }

360

);

361

362

const analysis = await analyzeResults(webResults);

363

364

agent.update({

365

output: { analysis, sources: webResults.length },

366

metadata: { processingTime: Date.now() }

367

});

368

369

return analysis;

370

},

371

{ asType: 'agent' }

372

);

373

```

374

375

### Tool

376

377

For individual tool calls and API interactions.

378

379

```typescript

380

const searchResults = await startActiveObservation(

381

'web-search',

382

async (tool) => {

383

tool.update({

384

input: { query: 'latest AI news', maxResults: 10 },

385

metadata: { provider: 'google-api' }

386

});

387

388

const results = await performWebSearch('latest AI news', 10);

389

390

tool.update({

391

output: {

392

results: results,

393

count: results.length,

394

relevanceScore: 0.89

395

},

396

metadata: {

397

latency: 1200,

398

cacheHit: false

399

}

400

});

401

402

return results;

403

},

404

{ asType: 'tool' }

405

);

406

```

407

408

### Retriever

409

410

For document retrieval and search operations.

411

412

```typescript

413

const documents = await startActiveObservation(

414

'vector-search',

415

async (retriever) => {

416

retriever.update({

417

input: {

418

query: 'machine learning algorithms',

419

topK: 5,

420

threshold: 0.7

421

},

422

metadata: {

423

vectorStore: 'pinecone',

424

embeddingModel: 'text-embedding-ada-002'

425

}

426

});

427

428

const results = await vectorDB.search({

429

query: 'machine learning algorithms',

430

topK: 5

431

});

432

433

retriever.update({

434

output: {

435

documents: results,

436

count: results.length,

437

avgSimilarity: 0.85

438

},

439

metadata: { searchLatency: 120 }

440

});

441

442

return results;

443

},

444

{ asType: 'retriever' }

445

);

446

```

447

448

### Evaluator

449

450

For quality assessment and evaluation.

451

452

```typescript

453

const evaluation = await startActiveObservation(

454

'response-evaluator',

455

async (evaluator) => {

456

evaluator.update({

457

input: {

458

response: 'Paris is the capital of France.',

459

reference: 'The capital city of France is Paris.',

460

criteria: ['accuracy', 'clarity']

461

},

462

metadata: { metric: 'semantic-similarity' }

463

});

464

465

const score = await calculateSimilarity(response, reference);

466

const passed = score > 0.8;

467

468

evaluator.update({

469

output: {

470

score,

471

passed,

472

grade: passed ? 'excellent' : 'needs_improvement'

473

}

474

});

475

476

return { score, passed };

477

},

478

{ asType: 'evaluator' }

479

);

480

```

481

482

### Guardrail

483

484

For safety checks and content filtering.

485

486

```typescript

487

const safetyCheck = await startActiveObservation(

488

'content-guardrail',

489

async (guardrail) => {

490

guardrail.update({

491

input: {

492

text: userMessage,

493

policies: ['no-profanity', 'no-pii']

494

},

495

metadata: { strictMode: true }

496

});

497

498

const violations = await checkContent(userMessage);

499

const allowed = violations.length === 0;

500

501

guardrail.update({

502

output: { allowed, violations, confidence: 0.95 }

503

});

504

505

return { allowed, violations };

506

},

507

{ asType: 'guardrail' }

508

);

509

```

510

511

### Embedding

512

513

For text embedding generation.

514

515

```typescript

516

const embeddings = await startActiveObservation(

517

'text-embeddings',

518

async (embedding) => {

519

const texts = ['Hello world', 'Machine learning'];

520

521

embedding.update({

522

input: { texts },

523

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

524

metadata: { dimensions: 1536 }

525

});

526

527

const vectors = await generateEmbeddings(texts);

528

529

embedding.update({

530

output: { embeddings: vectors, count: vectors.length },

531

usageDetails: { totalTokens: texts.join(' ').split(' ').length }

532

});

533

534

return vectors;

535

},

536

{ asType: 'embedding' }

537

);

538

```

539

540

## Advanced Options

541

542

### Custom Start Time

543

544

Specify a custom start time for the observation.

545

546

```typescript

547

const result = await startActiveObservation(

548

'backdated-operation',

549

async (observation) => {

550

observation.update({ input: { data: 'value' } });

551

const result = await process();

552

observation.update({ output: result });

553

return result;

554

},

555

{ startTime: new Date('2024-01-01T10:00:00Z') }

556

);

557

```

558

559

### Disable Automatic Ending

560

561

For long-running operations that need manual control.

562

563

```typescript

564

const observation = await startActiveObservation(

565

'background-process',

566

async (span) => {

567

span.update({ input: { taskId: '123' } });

568

569

// Start background task that continues after function returns

570

startBackgroundTask(span);

571

572

return 'process-started';

573

},

574

{ asType: 'span', endOnExit: false }

575

);

576

577

// Later, manually end the observation

578

// observation.end(); // Note: You need to keep a reference to the observation

579

```

580

581

### Parent Context

582

583

Link to a specific parent observation.

584

585

```typescript

586

const parentSpan = startObservation('parent');

587

const parentContext = parentSpan.otelSpan.spanContext();

588

589

await startActiveObservation(

590

'child-with-custom-parent',

591

async (child) => {

592

child.update({ input: { step: 'processing' } });

593

// Process...

594

child.update({ output: { result: 'done' } });

595

},

596

{ parentSpanContext: parentContext }

597

);

598

599

parentSpan.end();

600

```

601

602

## Context Propagation

603

604

Active observations automatically propagate context through the call stack.

605

606

```typescript

607

async function levelOne() {

608

return await startActiveObservation('level-1', async (obs1) => {

609

obs1.update({ input: { level: 1 } });

610

611

const result = await levelTwo();

612

613

obs1.update({ output: { result } });

614

return result;

615

});

616

}

617

618

async function levelTwo() {

619

// Automatically becomes child of level-1

620

return await startActiveObservation('level-2', async (obs2) => {

621

obs2.update({ input: { level: 2 } });

622

623

const result = await levelThree();

624

625

obs2.update({ output: { result } });

626

return result;

627

});

628

}

629

630

async function levelThree() {

631

// Automatically becomes child of level-2

632

return await startActiveObservation('level-3', async (obs3) => {

633

obs3.update({ input: { level: 3 } });

634

635

const result = await doWork();

636

637

obs3.update({ output: { result } });

638

return result;

639

});

640

}

641

642

// Creates a 3-level hierarchy automatically

643

await levelOne();

644

```

645

646

## Best Practices

647

648

### Use for Function-Scoped Operations

649

650

Active observations are ideal when the observation lifecycle matches a function's execution.

651

652

```typescript

653

// Good: Observation scope matches function scope

654

async function processOrder(orderId: string) {

655

return await startActiveObservation(

656

'process-order',

657

async (observation) => {

658

observation.update({ input: { orderId } });

659

// All processing happens within the observation

660

const result = await performProcessing(orderId);

661

observation.update({ output: result });

662

return result;

663

}

664

);

665

}

666

```

667

668

### Combine with Manual Observations

669

670

Mix manual and active observations based on needs.

671

672

```typescript

673

// Manual parent for long-lived workflow

674

const workflow = startObservation('long-workflow');

675

676

// Active child for scoped operation

677

const stepResult = await startActiveObservation(

678

'workflow-step',

679

async (step) => {

680

step.update({ input: { data: 'value' } });

681

const result = await processStep();

682

step.update({ output: result });

683

return result;

684

},

685

{ parentSpanContext: workflow.otelSpan.spanContext() }

686

);

687

688

// Continue with more work...

689

workflow.update({ output: { stepResult } });

690

workflow.end();

691

```

692

693

### Handle Errors Gracefully

694

695

Add try-catch blocks for custom error handling.

696

697

```typescript

698

await startActiveObservation(

699

'operation-with-recovery',

700

async (observation) => {

701

observation.update({ input: { attempt: 1 } });

702

703

try {

704

const result = await riskyOperation();

705

observation.update({ output: result });

706

return result;

707

} catch (error) {

708

observation.update({

709

level: 'ERROR',

710

statusMessage: error.message,

711

output: { error: error.message }

712

});

713

714

// Try recovery

715

const fallback = await fallbackOperation();

716

observation.update({

717

level: 'WARNING',

718

output: { fallback, recovered: true }

719

});

720

721

return fallback;

722

}

723

}

724

);

725

```

726

727

### Return Values

728

729

Always return values from the callback function to preserve function behavior.

730

731

```typescript

732

// Good: Return value preserved

733

const result = await startActiveObservation(

734

'calculation',

735

async (obs) => {

736

const value = await compute();

737

obs.update({ output: value });

738

return value; // Return the value

739

}

740

);

741

742

console.log(result); // Access the computed value

743

744

// Avoid: Missing return

745

await startActiveObservation('calculation', async (obs) => {

746

const value = await compute();

747

obs.update({ output: value });

748

// Missing return - caller won't get the value

749

});

750

```

751