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