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