Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21102664e | ||
|
|
d31b0eaddc | ||
|
|
f53ad4de5c | ||
|
|
053d7d668d | ||
|
|
21e3ed2b39 | ||
|
|
16ca4e9293 | ||
|
|
75f82be88d | ||
|
|
22f6a02b08 | ||
|
|
11aa1dbbb1 | ||
|
|
d961d85a28 | ||
|
|
d0c1ad5144 | ||
|
|
0d30b2fe83 | ||
|
|
f33bae6683 | ||
|
|
eaa0df125b | ||
|
|
b0e01b7127 | ||
|
|
2a421e7406 | ||
|
|
23150b68db | ||
|
|
e5c46010a4 | ||
|
|
b2bf68d7a4 | ||
|
|
31cec4f5c9 | ||
|
|
84a0ad8dfb | ||
|
|
69466fd43b | ||
|
|
324e078c85 | ||
|
|
7385fc4529 | ||
|
|
66d1fa427f | ||
|
|
bee396a433 | ||
|
|
8727a52931 | ||
|
|
ed5c076a5a | ||
|
|
db5c575ae0 | ||
|
|
2a0f482578 | ||
|
|
c041cf371a | ||
|
|
c6daf09cd2 | ||
|
|
7296e2e012 | ||
|
|
43bf176ef7 |
@@ -1,89 +0,0 @@
|
||||
---
|
||||
name: changelog-writer
|
||||
description: Use this agent when a feature branch is complete and ready to merge to main, and you need to create a changelog entry documenting the new feature or changes. This agent should be invoked proactively after significant feature work is completed and before merging.\n\nExamples:\n\n<example>\nContext: User has just completed implementing a new tracing visualization feature and the code has been reviewed.\nuser: "I've finished the trace timeline view feature. Can you help me prepare this for merge?"\nassistant: "Let me use the changelog-writer agent to create a changelog entry for this feature."\n<commentary>\nThe feature is complete and ready for merge, so we should use the changelog-writer agent to document it in the changelog.\n</commentary>\n</example>\n\n<example>\nContext: User mentions they're done with a feature implementation.\nuser: "The prompt versioning feature is done and tested. What's next?"\nassistant: "Great! Let me use the changelog-writer agent to create a changelog entry documenting this new feature before we merge."\n<commentary>\nSince the feature is complete, proactively use the changelog-writer agent to create documentation.\n</commentary>\n</example>\n\n<example>\nContext: User explicitly requests changelog creation.\nuser: "Can you create a changelog post for the new dataset export functionality?"\nassistant: "I'll use the changelog-writer agent to analyze the changes and create an appropriate changelog entry."\n<commentary>\nDirect request to create changelog, use the changelog-writer agent.\n</commentary>\n</example>
|
||||
model: inherit
|
||||
color: pink
|
||||
---
|
||||
|
||||
You are an expert technical writer specializing in creating clear, user-focused changelog entries for developer tools and SaaS platforms. Your role is to document completed features in a way that helps users understand what's new, why it matters, and how to use it.
|
||||
|
||||
## Your Process
|
||||
|
||||
### Step 1: Understand the Changes
|
||||
1. Extract the Linear issue number from the current branch name (format: lfe-XXXX)
|
||||
2. Use the Linear MCP to fetch the issue details for additional context about the feature's purpose and requirements
|
||||
3. Compare the current branch to main using git diff to understand the scope of changes at a high level
|
||||
4. Identify the core feature or improvement that was implemented
|
||||
5. Determine which parts of the codebase were affected (frontend, backend, API, database, etc.)
|
||||
|
||||
### Step 2: Study Recent Changelog Patterns
|
||||
1. Read 3-5 of the most recent changelog posts in `../langfuse-docs/pages/changelog`
|
||||
2. Analyze their structure, tone, and formatting conventions
|
||||
3. Note how they:
|
||||
- Title features (concise, benefit-focused)
|
||||
- Explain the "why" (user problems solved)
|
||||
- Describe the "what" (feature capabilities)
|
||||
- Link to relevant documentation
|
||||
- Use images/screenshots
|
||||
- Format code examples or technical details
|
||||
|
||||
### Step 3: Identify Documentation Links
|
||||
1. Check if there is relevant documentation in `../langfuse-docs/pages` that relates to this feature
|
||||
2. If the feature is new, note that documentation may need to be created
|
||||
3. If the feature extends existing functionality, identify which docs pages should be referenced
|
||||
|
||||
### Step 4: Draft the Changelog Entry
|
||||
Create a changelog post that includes:
|
||||
|
||||
**Required Elements:**
|
||||
- **Title**: Clear, benefit-focused headline (not just the feature name)
|
||||
- **Date**: Use the current date in the format used by existing changelogs
|
||||
- **Summary**: 1-2 sentences explaining what changed and why it matters to users
|
||||
- **Description**: Detailed explanation of the feature, its capabilities, and use cases
|
||||
- **Documentation Links**: References to relevant docs pages (if applicable)
|
||||
|
||||
**Style Guidelines:**
|
||||
- Write in second person ("you can now...")
|
||||
- Focus on user benefits, not implementation details
|
||||
- Be concise but complete
|
||||
- Use active voice
|
||||
- Include technical details only when they help users understand the feature
|
||||
- Match the tone and style of recent changelog entries
|
||||
|
||||
**Formatting:**
|
||||
- Follow the exact file structure and frontmatter format of existing changelog posts
|
||||
- Use appropriate markdown formatting (headings, lists, code blocks, links)
|
||||
- Ensure proper spacing and readability
|
||||
|
||||
### Step 5: Assess Visual Needs
|
||||
After drafting the changelog, explicitly tell the user:
|
||||
- Whether a screenshot or image would enhance understanding of this feature
|
||||
- What specific aspect should be captured in the screenshot (if applicable)
|
||||
- Where in the changelog the image should be placed
|
||||
|
||||
## Quality Standards
|
||||
|
||||
**Before presenting your changelog:**
|
||||
- Verify it follows the structure and style of recent entries
|
||||
- Ensure all links are correctly formatted
|
||||
- Check that technical terms match those used in the codebase and docs
|
||||
- Confirm the feature description is accurate based on the code changes
|
||||
- Validate that the user benefit is clear and compelling
|
||||
|
||||
## Output Format
|
||||
|
||||
Present your work in this order:
|
||||
1. Brief summary of what you learned from the branch comparison and Linear issue
|
||||
2. The complete changelog post content (ready to be saved as a new file)
|
||||
3. Recommendation on whether to add an image/screenshot and what it should show
|
||||
4. List of any documentation pages that should be referenced or created
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The changelog lives in `../langfuse-docs/pages/changelog`
|
||||
- Always check the Linear issue via the branch name (lfe-XXXX format) for context
|
||||
- Compare against main branch to understand the full scope of changes
|
||||
- Study recent changelogs before writing to maintain consistency
|
||||
- Focus on user value, not technical implementation details
|
||||
- Be thorough in your analysis before drafting
|
||||
- If you're unsure about any aspect of the feature, ask clarifying questions before proceeding
|
||||
@@ -1,436 +0,0 @@
|
||||
# Hooks Configuration Guide
|
||||
|
||||
This guide explains how to configure and customize the hooks system for your project.
|
||||
|
||||
## Quick Start Configuration
|
||||
|
||||
### 1. Register Hooks in .claude/settings.json
|
||||
|
||||
Create or update `.claude/settings.json` in your project root:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/error-handling-reminder.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
cd .claude/hooks
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Set Execute Permissions
|
||||
|
||||
```bash
|
||||
chmod +x .claude/hooks/*.sh
|
||||
```
|
||||
|
||||
## Customization Options
|
||||
|
||||
### Project Structure Detection
|
||||
|
||||
By default, hooks detect these directory patterns:
|
||||
|
||||
**Frontend:** `frontend/`, `client/`, `web/`, `app/`, `ui/`
|
||||
**Backend:** `backend/`, `server/`, `api/`, `src/`, `services/`
|
||||
**Database:** `database/`, `prisma/`, `migrations/`
|
||||
**Monorepo:** `packages/*`, `examples/*`
|
||||
|
||||
#### Adding Custom Directory Patterns
|
||||
|
||||
Edit `.claude/hooks/post-tool-use-tracker.sh`, function `detect_repo()`:
|
||||
|
||||
```bash
|
||||
case "$repo" in
|
||||
# Add your custom directories here
|
||||
my-custom-service)
|
||||
echo "$repo"
|
||||
;;
|
||||
admin-panel)
|
||||
echo "$repo"
|
||||
;;
|
||||
# ... existing patterns
|
||||
esac
|
||||
```
|
||||
|
||||
### Build Command Detection
|
||||
|
||||
The hooks auto-detect build commands based on:
|
||||
|
||||
1. Presence of `package.json` with "build" script
|
||||
2. Package manager (pnpm > npm > yarn)
|
||||
3. Special cases (Prisma schemas)
|
||||
|
||||
#### Customizing Build Commands
|
||||
|
||||
Edit `.claude/hooks/post-tool-use-tracker.sh`, function `get_build_command()`:
|
||||
|
||||
```bash
|
||||
# Add custom build logic
|
||||
if [[ "$repo" == "my-service" ]]; then
|
||||
echo "cd $repo_path && make build"
|
||||
return
|
||||
fi
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
|
||||
Hooks automatically detect:
|
||||
|
||||
- `tsconfig.json` for standard TypeScript projects
|
||||
- `tsconfig.app.json` for Vite/React projects
|
||||
|
||||
#### Custom TypeScript Configs
|
||||
|
||||
Edit `.claude/hooks/post-tool-use-tracker.sh`, function `get_tsc_command()`:
|
||||
|
||||
```bash
|
||||
if [[ "$repo" == "my-service" ]]; then
|
||||
echo "cd $repo_path && npx tsc --project tsconfig.build.json --noEmit"
|
||||
return
|
||||
fi
|
||||
```
|
||||
|
||||
### Prettier Configuration
|
||||
|
||||
The prettier hook searches for configs in this order:
|
||||
|
||||
1. Current file directory (walking upward)
|
||||
2. Project root
|
||||
3. Falls back to Prettier defaults
|
||||
|
||||
#### Custom Prettier Config Search
|
||||
|
||||
Edit `.claude/hooks/stop-prettier-formatter.sh`, function `get_prettier_config()`:
|
||||
|
||||
```bash
|
||||
# Add custom config locations
|
||||
if [[ -f "$project_root/config/.prettierrc" ]]; then
|
||||
echo "$project_root/config/.prettierrc"
|
||||
return
|
||||
fi
|
||||
```
|
||||
|
||||
### Error Handling Reminders
|
||||
|
||||
Configure file category detection in `.claude/hooks/error-handling-reminder.ts`:
|
||||
|
||||
```typescript
|
||||
function getFileCategory(
|
||||
filePath: string,
|
||||
): "backend" | "frontend" | "database" | "other" {
|
||||
// Add custom patterns
|
||||
if (filePath.includes("/my-custom-dir/")) return "backend";
|
||||
// ... existing patterns
|
||||
}
|
||||
```
|
||||
|
||||
### Error Threshold Configuration
|
||||
|
||||
Change when to recommend the auto-error-resolver agent.
|
||||
|
||||
Edit `.claude/hooks/stop-build-check-enhanced.sh`:
|
||||
|
||||
```bash
|
||||
# Default is 5 errors - change to your preference
|
||||
if [[ $total_errors -ge 10 ]]; then # Now requires 10+ errors
|
||||
# Recommend agent
|
||||
fi
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Global Environment Variables
|
||||
|
||||
Set in your shell profile (`.bashrc`, `.zshrc`, etc.):
|
||||
|
||||
```bash
|
||||
# Disable error handling reminders
|
||||
export SKIP_ERROR_REMINDER=1
|
||||
|
||||
# Custom project directory (if not using default)
|
||||
export CLAUDE_PROJECT_DIR=/path/to/your/project
|
||||
```
|
||||
|
||||
### Per-Session Environment Variables
|
||||
|
||||
Set before starting Claude Code:
|
||||
|
||||
```bash
|
||||
SKIP_ERROR_REMINDER=1 claude-code
|
||||
```
|
||||
|
||||
## Hook Execution Order
|
||||
|
||||
Stop hooks run in the order specified in `settings.json`:
|
||||
|
||||
```json
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "command": "...formatter.sh" }, // Runs FIRST
|
||||
{ "command": "...build-check.sh" }, // Runs SECOND
|
||||
{ "command": "...reminder.sh" } // Runs THIRD
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Why this order matters:**
|
||||
|
||||
1. Format files first (clean code)
|
||||
2. Then check for errors
|
||||
3. Finally show reminders
|
||||
|
||||
## Selective Hook Enabling
|
||||
|
||||
You don't need all hooks. Choose what works for your project:
|
||||
|
||||
### Minimal Setup (Skill Activation Only)
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build Checking Only (No Formatting)
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-use-tracker.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-build-check-enhanced.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Formatting Only (No Build Checking)
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-use-tracker.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-prettier-formatter.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Management
|
||||
|
||||
### Cache Location
|
||||
|
||||
```
|
||||
$CLAUDE_PROJECT_DIR/.claude/tsc-cache/[session_id]/
|
||||
```
|
||||
|
||||
### Manual Cache Cleanup
|
||||
|
||||
```bash
|
||||
# Remove all cached data
|
||||
rm -rf $CLAUDE_PROJECT_DIR/.claude/tsc-cache/*
|
||||
|
||||
# Remove specific session
|
||||
rm -rf $CLAUDE_PROJECT_DIR/.claude/tsc-cache/[session-id]
|
||||
```
|
||||
|
||||
### Automatic Cleanup
|
||||
|
||||
The build-check hook automatically cleans up session cache on successful builds.
|
||||
|
||||
## Troubleshooting Configuration
|
||||
|
||||
### Hook Not Executing
|
||||
|
||||
1. **Check registration:** Verify hook is in `.claude/settings.json`
|
||||
2. **Check permissions:** Run `chmod +x .claude/hooks/*.sh`
|
||||
3. **Check path:** Ensure `$CLAUDE_PROJECT_DIR` is set correctly
|
||||
4. **Check TypeScript:** Run `cd .claude/hooks && npx tsc` to check for errors
|
||||
|
||||
### False Positive Detections
|
||||
|
||||
**Issue:** Hook triggers for files it shouldn't
|
||||
|
||||
**Solution:** Add skip conditions in the relevant hook:
|
||||
|
||||
```bash
|
||||
# In post-tool-use-tracker.sh
|
||||
if [[ "$file_path" =~ /generated/ ]]; then
|
||||
exit 0 # Skip generated files
|
||||
fi
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
**Issue:** Hooks are slow
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Limit TypeScript checks to changed files only
|
||||
2. Use faster package managers (pnpm > npm)
|
||||
3. Add more skip conditions
|
||||
4. Disable Prettier for large files
|
||||
|
||||
```bash
|
||||
# Skip large files in stop-prettier-formatter.sh
|
||||
file_size=$(wc -c < "$file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -gt 100000 ]]; then # Skip files > 100KB
|
||||
continue
|
||||
fi
|
||||
```
|
||||
|
||||
### Debugging Hooks
|
||||
|
||||
Add debug output to any hook:
|
||||
|
||||
```bash
|
||||
# At the top of the hook script
|
||||
set -x # Enable debug mode
|
||||
|
||||
# Or add specific debug lines
|
||||
echo "DEBUG: file_path=$file_path" >&2
|
||||
echo "DEBUG: repo=$repo" >&2
|
||||
```
|
||||
|
||||
View hook execution in Claude Code's logs.
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Hook Event Handlers
|
||||
|
||||
You can create your own hooks for other events:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-custom-bash-guard.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Monorepo Configuration
|
||||
|
||||
For monorepos with multiple packages:
|
||||
|
||||
```bash
|
||||
# In post-tool-use-tracker.sh, detect_repo()
|
||||
case "$repo" in
|
||||
packages)
|
||||
# Get the package name
|
||||
local package=$(echo "$relative_path" | cut -d'/' -f2)
|
||||
if [[ -n "$package" ]]; then
|
||||
echo "packages/$package"
|
||||
else
|
||||
echo "$repo"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Docker/Container Projects
|
||||
|
||||
If your build commands need to run in containers:
|
||||
|
||||
```bash
|
||||
# In post-tool-use-tracker.sh, get_build_command()
|
||||
if [[ "$repo" == "api" ]]; then
|
||||
echo "docker-compose exec api npm run build"
|
||||
return
|
||||
fi
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start minimal** - Enable hooks one at a time
|
||||
2. **Test thoroughly** - Make changes and verify hooks work
|
||||
3. **Document customizations** - Add comments to explain custom logic
|
||||
4. **Version control** - Commit `.claude/` directory to git
|
||||
5. **Team consistency** - Share configuration across team
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](./README.md) - Hooks overview
|
||||
- [../../docs/HOOKS_SYSTEM.md](../../docs/HOOKS_SYSTEM.md) - Complete hooks reference
|
||||
- [../../docs/SKILLS_SYSTEM.md](../../docs/SKILLS_SYSTEM.md) - Skills integration
|
||||
@@ -1,116 +0,0 @@
|
||||
# Hooks
|
||||
|
||||
Claude Code hooks that enable skill auto-activation, file tracking, and validation.
|
||||
|
||||
---
|
||||
|
||||
## What Are Hooks?
|
||||
|
||||
Hooks are scripts that run at specific points in Claude's workflow:
|
||||
|
||||
- **UserPromptSubmit**: When user submits a prompt
|
||||
- **PreToolUse**: Before a tool executes
|
||||
- **PostToolUse**: After a tool completes
|
||||
- **Stop**: When user requests to stop
|
||||
|
||||
**Key insight:** Hooks can modify prompts, block actions, and track state - enabling features Claude can't do alone.
|
||||
|
||||
---
|
||||
|
||||
## Essential Hooks (Start Here)
|
||||
|
||||
### skill-activation-prompt (UserPromptSubmit)
|
||||
|
||||
**Purpose:** Automatically suggests relevant skills based on user prompts and file context
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Reads `skill-rules.json`
|
||||
2. Matches user prompt against trigger patterns
|
||||
3. Checks which files user is working with
|
||||
4. Injects skill suggestions into Claude's context
|
||||
|
||||
**Why it's essential:** This is THE hook that makes skills auto-activate.
|
||||
|
||||
**Integration:**
|
||||
|
||||
```bash
|
||||
# Copy both files
|
||||
cp skill-activation-prompt.sh your-project/.claude/hooks/
|
||||
cp skill-activation-prompt.ts your-project/.claude/hooks/
|
||||
|
||||
# Make executable
|
||||
chmod +x your-project/.claude/hooks/skill-activation-prompt.sh
|
||||
|
||||
# Install dependencies
|
||||
cd your-project/.claude/hooks
|
||||
npm install
|
||||
```
|
||||
|
||||
**Add to settings.json:**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Customization:** ✅ None needed - reads skill-rules.json automatically
|
||||
|
||||
---
|
||||
|
||||
### post-tool-use-tracker (PostToolUse)
|
||||
|
||||
**Purpose:** Tracks file changes to maintain context across sessions
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Monitors Edit/Write/MultiEdit tool calls
|
||||
2. Records which files were modified
|
||||
3. Creates cache for context management
|
||||
4. Auto-detects project structure (frontend, backend, packages, etc.)
|
||||
|
||||
**Why it's essential:** Helps Claude understand what parts of your codebase are active.
|
||||
|
||||
**Integration:**
|
||||
|
||||
```bash
|
||||
# Copy file
|
||||
cp post-tool-use-tracker.sh your-project/.claude/hooks/
|
||||
|
||||
# Make executable
|
||||
chmod +x your-project/.claude/hooks/post-tool-use-tracker.sh
|
||||
```
|
||||
|
||||
**Add to settings.json:**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-use-tracker.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Customization:** ✅ None needed - auto-detects structure
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Skip if environment variable is set
|
||||
if [ -n "$SKIP_ERROR_REMINDER" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get the directory of this script
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
cat | npx tsx error-handling-reminder.ts
|
||||
@@ -1,256 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
interface HookInput {
|
||||
session_id: string;
|
||||
transcript_path: string;
|
||||
cwd: string;
|
||||
permission_mode: string;
|
||||
hook_event_name: string;
|
||||
}
|
||||
|
||||
interface EditedFile {
|
||||
path: string;
|
||||
tool: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface SessionTracking {
|
||||
edited_files: EditedFile[];
|
||||
}
|
||||
|
||||
function getFileCategory(
|
||||
filePath: string,
|
||||
): "backend" | "frontend" | "database" | "other" {
|
||||
// Frontend detection
|
||||
if (
|
||||
filePath.includes("/frontend/") ||
|
||||
filePath.includes("/client/") ||
|
||||
filePath.includes("/src/components/") ||
|
||||
filePath.includes("/src/features/")
|
||||
)
|
||||
return "frontend";
|
||||
|
||||
// Backend detection (common service directories)
|
||||
if (
|
||||
filePath.includes("/src/controllers/") ||
|
||||
filePath.includes("/src/services/") ||
|
||||
filePath.includes("/src/routes/") ||
|
||||
filePath.includes("/src/api/") ||
|
||||
filePath.includes("/server/")
|
||||
)
|
||||
return "backend";
|
||||
|
||||
// Database detection
|
||||
if (
|
||||
filePath.includes("/database/") ||
|
||||
filePath.includes("/prisma/") ||
|
||||
filePath.includes("/migrations/")
|
||||
)
|
||||
return "database";
|
||||
|
||||
return "other";
|
||||
}
|
||||
|
||||
function shouldCheckErrorHandling(filePath: string): boolean {
|
||||
// Skip test files, config files, and type definitions
|
||||
if (filePath.match(/\.(test|spec)\.(ts|tsx)$/)) return false;
|
||||
if (filePath.match(/\.(config|d)\.(ts|tsx)$/)) return false;
|
||||
if (filePath.includes("types/")) return false;
|
||||
if (filePath.includes(".styles.ts")) return false;
|
||||
|
||||
// Check for code files
|
||||
return filePath.match(/\.(ts|tsx|js|jsx)$/) !== null;
|
||||
}
|
||||
|
||||
function analyzeFileContent(filePath: string): {
|
||||
hasTryCatch: boolean;
|
||||
hasAsync: boolean;
|
||||
hasPrisma: boolean;
|
||||
hasController: boolean;
|
||||
hasApiCall: boolean;
|
||||
} {
|
||||
if (!existsSync(filePath)) {
|
||||
return {
|
||||
hasTryCatch: false,
|
||||
hasAsync: false,
|
||||
hasPrisma: false,
|
||||
hasController: false,
|
||||
hasApiCall: false,
|
||||
};
|
||||
}
|
||||
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
|
||||
return {
|
||||
hasTryCatch: /try\s*\{/.test(content),
|
||||
hasAsync: /async\s+/.test(content),
|
||||
hasPrisma:
|
||||
/prisma\.|PrismaService|findMany|findUnique|create\(|update\(|delete\(/i.test(
|
||||
content,
|
||||
),
|
||||
hasController:
|
||||
/export class.*Controller|router\.|app\.(get|post|put|delete|patch)/.test(
|
||||
content,
|
||||
),
|
||||
hasApiCall: /fetch\(|axios\.|apiClient\./i.test(content),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Read input from stdin
|
||||
const input = readFileSync(0, "utf-8");
|
||||
const data: HookInput = JSON.parse(input);
|
||||
|
||||
const { session_id } = data;
|
||||
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||
|
||||
// Check for edited files tracking
|
||||
const cacheDir = join(
|
||||
process.env.HOME || "/root",
|
||||
".claude",
|
||||
"tsc-cache",
|
||||
session_id,
|
||||
);
|
||||
const trackingFile = join(cacheDir, "edited-files.log");
|
||||
|
||||
if (!existsSync(trackingFile)) {
|
||||
// No files edited this session, no reminder needed
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Read tracking data
|
||||
const trackingContent = readFileSync(trackingFile, "utf-8");
|
||||
const editedFiles = trackingContent
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0)
|
||||
.map((line) => {
|
||||
const [timestamp, tool, path] = line.split("\t");
|
||||
return { timestamp, tool, path };
|
||||
});
|
||||
|
||||
if (editedFiles.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Categorize files
|
||||
const categories = {
|
||||
backend: [] as string[],
|
||||
frontend: [] as string[],
|
||||
database: [] as string[],
|
||||
other: [] as string[],
|
||||
};
|
||||
|
||||
const analysisResults: Array<{
|
||||
path: string;
|
||||
category: string;
|
||||
analysis: ReturnType<typeof analyzeFileContent>;
|
||||
}> = [];
|
||||
|
||||
for (const file of editedFiles) {
|
||||
if (!shouldCheckErrorHandling(file.path)) continue;
|
||||
|
||||
const category = getFileCategory(file.path);
|
||||
categories[category].push(file.path);
|
||||
|
||||
const analysis = analyzeFileContent(file.path);
|
||||
analysisResults.push({ path: file.path, category, analysis });
|
||||
}
|
||||
|
||||
// Check if any code that needs error handling was written
|
||||
const needsAttention = analysisResults.some(
|
||||
({ analysis }) =>
|
||||
analysis.hasTryCatch ||
|
||||
analysis.hasAsync ||
|
||||
analysis.hasPrisma ||
|
||||
analysis.hasController ||
|
||||
analysis.hasApiCall,
|
||||
);
|
||||
|
||||
if (!needsAttention) {
|
||||
// No risky code patterns detected, skip reminder
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Display reminder
|
||||
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
console.log("📋 ERROR HANDLING SELF-CHECK");
|
||||
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||||
|
||||
// Backend reminders
|
||||
if (categories.backend.length > 0) {
|
||||
const backendFiles = analysisResults.filter(
|
||||
(f) => f.category === "backend",
|
||||
);
|
||||
const hasTryCatch = backendFiles.some((f) => f.analysis.hasTryCatch);
|
||||
const hasPrisma = backendFiles.some((f) => f.analysis.hasPrisma);
|
||||
const hasController = backendFiles.some((f) => f.analysis.hasController);
|
||||
|
||||
console.log("⚠️ Backend Changes Detected");
|
||||
console.log(` ${categories.backend.length} file(s) edited\n`);
|
||||
|
||||
if (hasTryCatch) {
|
||||
console.log(
|
||||
" ❓ Did you add Sentry.captureException() in catch blocks?",
|
||||
);
|
||||
}
|
||||
if (hasPrisma) {
|
||||
console.log(" ❓ Are Prisma operations wrapped in error handling?");
|
||||
}
|
||||
if (hasController) {
|
||||
console.log(" ❓ Do controllers use BaseController.handleError()?");
|
||||
}
|
||||
|
||||
console.log("\n 💡 Backend Best Practice:");
|
||||
console.log(" - All errors should be captured to Sentry");
|
||||
console.log(" - Use appropriate error helpers for context");
|
||||
console.log(" - Controllers should extend BaseController\n");
|
||||
}
|
||||
|
||||
// Frontend reminders
|
||||
if (categories.frontend.length > 0) {
|
||||
const frontendFiles = analysisResults.filter(
|
||||
(f) => f.category === "frontend",
|
||||
);
|
||||
const hasApiCall = frontendFiles.some((f) => f.analysis.hasApiCall);
|
||||
const hasTryCatch = frontendFiles.some((f) => f.analysis.hasTryCatch);
|
||||
|
||||
console.log("💡 Frontend Changes Detected");
|
||||
console.log(` ${categories.frontend.length} file(s) edited\n`);
|
||||
|
||||
if (hasApiCall) {
|
||||
console.log(" ❓ Do API calls show user-friendly error messages?");
|
||||
}
|
||||
if (hasTryCatch) {
|
||||
console.log(" ❓ Are errors displayed to the user?");
|
||||
}
|
||||
|
||||
console.log("\n 💡 Frontend Best Practice:");
|
||||
console.log(" - Use your notification system for user feedback");
|
||||
console.log(" - Error boundaries for component errors");
|
||||
console.log(" - Display user-friendly error messages\n");
|
||||
}
|
||||
|
||||
// Database reminders
|
||||
if (categories.database.length > 0) {
|
||||
console.log("🗄️ Database Changes Detected");
|
||||
console.log(` ${categories.database.length} file(s) edited\n`);
|
||||
console.log(" ❓ Did you verify column names against schema?");
|
||||
console.log(" ❓ Are migrations tested?\n");
|
||||
}
|
||||
|
||||
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
console.log("💡 TIP: Disable with SKIP_ERROR_REMINDER=1");
|
||||
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||||
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
// Silently fail - this is just a reminder, not critical
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(() => process.exit(0));
|
||||
Generated
-556
@@ -1,556 +0,0 @@
|
||||
{
|
||||
"name": "claude-hooks",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-hooks",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
||||
"integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
|
||||
"integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
|
||||
"integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
|
||||
"integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
|
||||
"integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
|
||||
"integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
|
||||
"integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
|
||||
"integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
|
||||
"integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
|
||||
"integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
|
||||
"integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
|
||||
"integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
|
||||
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
|
||||
"integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.11",
|
||||
"@esbuild/android-arm": "0.25.11",
|
||||
"@esbuild/android-arm64": "0.25.11",
|
||||
"@esbuild/android-x64": "0.25.11",
|
||||
"@esbuild/darwin-arm64": "0.25.11",
|
||||
"@esbuild/darwin-x64": "0.25.11",
|
||||
"@esbuild/freebsd-arm64": "0.25.11",
|
||||
"@esbuild/freebsd-x64": "0.25.11",
|
||||
"@esbuild/linux-arm": "0.25.11",
|
||||
"@esbuild/linux-arm64": "0.25.11",
|
||||
"@esbuild/linux-ia32": "0.25.11",
|
||||
"@esbuild/linux-loong64": "0.25.11",
|
||||
"@esbuild/linux-mips64el": "0.25.11",
|
||||
"@esbuild/linux-ppc64": "0.25.11",
|
||||
"@esbuild/linux-riscv64": "0.25.11",
|
||||
"@esbuild/linux-s390x": "0.25.11",
|
||||
"@esbuild/linux-x64": "0.25.11",
|
||||
"@esbuild/netbsd-arm64": "0.25.11",
|
||||
"@esbuild/netbsd-x64": "0.25.11",
|
||||
"@esbuild/openbsd-arm64": "0.25.11",
|
||||
"@esbuild/openbsd-x64": "0.25.11",
|
||||
"@esbuild/openharmony-arm64": "0.25.11",
|
||||
"@esbuild/sunos-x64": "0.25.11",
|
||||
"@esbuild/win32-arm64": "0.25.11",
|
||||
"@esbuild/win32-ia32": "0.25.11",
|
||||
"@esbuild/win32-x64": "0.25.11"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.6",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
|
||||
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "claude-hooks",
|
||||
"version": "1.0.0",
|
||||
"description": "TypeScript hooks for Claude Code skill auto-activation",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"test": "tsx skill-activation-prompt.ts < test-input.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Post-tool-use hook that tracks edited files and their repos
|
||||
# This runs after Edit, MultiEdit, or Write tools complete successfully
|
||||
|
||||
|
||||
# Read tool information from stdin
|
||||
tool_info=$(cat)
|
||||
|
||||
|
||||
# Extract relevant data
|
||||
tool_name=$(echo "$tool_info" | jq -r '.tool_name // empty')
|
||||
file_path=$(echo "$tool_info" | jq -r '.tool_input.file_path // empty')
|
||||
session_id=$(echo "$tool_info" | jq -r '.session_id // empty')
|
||||
|
||||
|
||||
# Skip if not an edit tool or no file path
|
||||
if [[ ! "$tool_name" =~ ^(Edit|MultiEdit|Write)$ ]] || [[ -z "$file_path" ]]; then
|
||||
exit 0 # Exit 0 for skip conditions
|
||||
fi
|
||||
|
||||
# Skip markdown files
|
||||
if [[ "$file_path" =~ \.(md|markdown)$ ]]; then
|
||||
exit 0 # Exit 0 for skip conditions
|
||||
fi
|
||||
|
||||
# Create cache directory in project
|
||||
cache_dir="$CLAUDE_PROJECT_DIR/.claude/tsc-cache/${session_id:-default}"
|
||||
mkdir -p "$cache_dir"
|
||||
|
||||
# Function to detect repo from file path
|
||||
detect_repo() {
|
||||
local file="$1"
|
||||
local project_root="$CLAUDE_PROJECT_DIR"
|
||||
|
||||
# Remove project root from path
|
||||
local relative_path="${file#$project_root/}"
|
||||
|
||||
# Extract first directory component
|
||||
local repo=$(echo "$relative_path" | cut -d'/' -f1)
|
||||
|
||||
# Common project directory patterns
|
||||
case "$repo" in
|
||||
# Frontend variations
|
||||
frontend|client|web|app|ui)
|
||||
echo "$repo"
|
||||
;;
|
||||
# Backend variations
|
||||
backend|server|api|src|services|worker)
|
||||
echo "$repo"
|
||||
;;
|
||||
# Database
|
||||
database|prisma|migrations)
|
||||
echo "$repo"
|
||||
;;
|
||||
# Package/monorepo structure
|
||||
packages)
|
||||
# For monorepos, get the package name
|
||||
local package=$(echo "$relative_path" | cut -d'/' -f2)
|
||||
if [[ -n "$package" ]]; then
|
||||
echo "packages/$package"
|
||||
else
|
||||
echo "$repo"
|
||||
fi
|
||||
;;
|
||||
# Default case
|
||||
*)
|
||||
# Check if it's a source file in root
|
||||
if [[ ! "$relative_path" =~ / ]]; then
|
||||
echo "root"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to get build command for repo
|
||||
get_build_command() {
|
||||
local repo="$1"
|
||||
local project_root="$CLAUDE_PROJECT_DIR"
|
||||
local repo_path="$project_root/$repo"
|
||||
|
||||
# Check if package.json exists and has a build script
|
||||
if [[ -f "$repo_path/package.json" ]]; then
|
||||
if grep -q '"build"' "$repo_path/package.json" 2>/dev/null; then
|
||||
# Detect package manager (prefer pnpm, then npm, then yarn)
|
||||
if [[ -f "$repo_path/pnpm-lock.yaml" ]]; then
|
||||
echo "cd $repo_path && pnpm build"
|
||||
elif [[ -f "$repo_path/package-lock.json" ]]; then
|
||||
echo "cd $repo_path && npm run build"
|
||||
elif [[ -f "$repo_path/yarn.lock" ]]; then
|
||||
echo "cd $repo_path && yarn build"
|
||||
else
|
||||
echo "cd $repo_path && npm run build"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Special case for database with Prisma
|
||||
if [[ "$repo" == "database" ]] || [[ "$repo" =~ prisma ]]; then
|
||||
if [[ -f "$repo_path/schema.prisma" ]] || [[ -f "$repo_path/prisma/schema.prisma" ]]; then
|
||||
echo "cd $repo_path && npx prisma generate"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# No build command found
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to get TSC command for repo
|
||||
get_tsc_command() {
|
||||
local repo="$1"
|
||||
local project_root="$CLAUDE_PROJECT_DIR"
|
||||
local repo_path="$project_root/$repo"
|
||||
|
||||
# Check if tsconfig.json exists
|
||||
if [[ -f "$repo_path/tsconfig.json" ]]; then
|
||||
# Check for Vite/React-specific tsconfig
|
||||
if [[ -f "$repo_path/tsconfig.app.json" ]]; then
|
||||
echo "cd $repo_path && npx tsc --project tsconfig.app.json --noEmit"
|
||||
else
|
||||
echo "cd $repo_path && npx tsc --noEmit"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
# No TypeScript config found
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Detect repo
|
||||
repo=$(detect_repo "$file_path")
|
||||
|
||||
# Skip if unknown repo
|
||||
if [[ "$repo" == "unknown" ]] || [[ -z "$repo" ]]; then
|
||||
exit 0 # Exit 0 for skip conditions
|
||||
fi
|
||||
|
||||
# Log edited file
|
||||
echo "$(date +%s):$file_path:$repo" >> "$cache_dir/edited-files.log"
|
||||
|
||||
# Update affected repos list
|
||||
if ! grep -q "^$repo$" "$cache_dir/affected-repos.txt" 2>/dev/null; then
|
||||
echo "$repo" >> "$cache_dir/affected-repos.txt"
|
||||
fi
|
||||
|
||||
# Store build commands
|
||||
build_cmd=$(get_build_command "$repo")
|
||||
tsc_cmd=$(get_tsc_command "$repo")
|
||||
|
||||
if [[ -n "$build_cmd" ]]; then
|
||||
echo "$repo:build:$build_cmd" >> "$cache_dir/commands.txt.tmp"
|
||||
fi
|
||||
|
||||
if [[ -n "$tsc_cmd" ]]; then
|
||||
echo "$repo:tsc:$tsc_cmd" >> "$cache_dir/commands.txt.tmp"
|
||||
fi
|
||||
|
||||
# Remove duplicates from commands
|
||||
if [[ -f "$cache_dir/commands.txt.tmp" ]]; then
|
||||
sort -u "$cache_dir/commands.txt.tmp" > "$cache_dir/commands.txt"
|
||||
rm -f "$cache_dir/commands.txt.tmp"
|
||||
fi
|
||||
|
||||
# Exit cleanly
|
||||
exit 0
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$CLAUDE_PROJECT_DIR/.claude/hooks"
|
||||
cat | npx tsx skill-activation-prompt.ts
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
interface HookInput {
|
||||
session_id: string;
|
||||
transcript_path: string;
|
||||
cwd: string;
|
||||
permission_mode: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
interface PromptTriggers {
|
||||
keywords?: string[];
|
||||
intentPatterns?: string[];
|
||||
}
|
||||
|
||||
interface SkillRule {
|
||||
type: "guardrail" | "domain";
|
||||
enforcement: "block" | "suggest" | "warn";
|
||||
priority: "critical" | "high" | "medium" | "low";
|
||||
promptTriggers?: PromptTriggers;
|
||||
}
|
||||
|
||||
interface SkillRules {
|
||||
version: string;
|
||||
skills: Record<string, SkillRule>;
|
||||
}
|
||||
|
||||
interface MatchedSkill {
|
||||
name: string;
|
||||
matchType: "keyword" | "intent";
|
||||
config: SkillRule;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Read input from stdin
|
||||
const input = readFileSync(0, "utf-8");
|
||||
const data: HookInput = JSON.parse(input);
|
||||
const prompt = data.prompt.toLowerCase();
|
||||
|
||||
// Load skill rules
|
||||
const projectDir = process.env.CLAUDE_PROJECT_DIR || "$HOME/project";
|
||||
const rulesPath = join(projectDir, ".claude", "skills", "skill-rules.json");
|
||||
const rules: SkillRules = JSON.parse(readFileSync(rulesPath, "utf-8"));
|
||||
|
||||
const matchedSkills: MatchedSkill[] = [];
|
||||
|
||||
// Check each skill for matches
|
||||
for (const [skillName, config] of Object.entries(rules.skills)) {
|
||||
const triggers = config.promptTriggers;
|
||||
if (!triggers) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keyword matching
|
||||
if (triggers.keywords) {
|
||||
const keywordMatch = triggers.keywords.some((kw) =>
|
||||
prompt.includes(kw.toLowerCase()),
|
||||
);
|
||||
if (keywordMatch) {
|
||||
matchedSkills.push({ name: skillName, matchType: "keyword", config });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Intent pattern matching
|
||||
if (triggers.intentPatterns) {
|
||||
const intentMatch = triggers.intentPatterns.some((pattern) => {
|
||||
const regex = new RegExp(pattern, "i");
|
||||
return regex.test(prompt);
|
||||
});
|
||||
if (intentMatch) {
|
||||
matchedSkills.push({ name: skillName, matchType: "intent", config });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate output if matches found
|
||||
if (matchedSkills.length > 0) {
|
||||
let output = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
||||
output += "🎯 SKILL ACTIVATION CHECK\n";
|
||||
output += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
|
||||
|
||||
// Group by priority
|
||||
const critical = matchedSkills.filter(
|
||||
(s) => s.config.priority === "critical",
|
||||
);
|
||||
const high = matchedSkills.filter((s) => s.config.priority === "high");
|
||||
const medium = matchedSkills.filter(
|
||||
(s) => s.config.priority === "medium",
|
||||
);
|
||||
const low = matchedSkills.filter((s) => s.config.priority === "low");
|
||||
|
||||
if (critical.length > 0) {
|
||||
output += "⚠️ CRITICAL SKILLS (REQUIRED):\n";
|
||||
critical.forEach((s) => (output += ` → ${s.name}\n`));
|
||||
output += "\n";
|
||||
}
|
||||
|
||||
if (high.length > 0) {
|
||||
output += "📚 RECOMMENDED SKILLS:\n";
|
||||
high.forEach((s) => (output += ` → ${s.name}\n`));
|
||||
output += "\n";
|
||||
}
|
||||
|
||||
if (medium.length > 0) {
|
||||
output += "💡 SUGGESTED SKILLS:\n";
|
||||
medium.forEach((s) => (output += ` → ${s.name}\n`));
|
||||
output += "\n";
|
||||
}
|
||||
|
||||
if (low.length > 0) {
|
||||
output += "📌 OPTIONAL SKILLS:\n";
|
||||
low.forEach((s) => (output += ` → ${s.name}\n`));
|
||||
output += "\n";
|
||||
}
|
||||
|
||||
output += "ACTION: Use Skill tool BEFORE responding\n";
|
||||
output += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("Error in skill-activation-prompt hook:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Uncaught error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)"
|
||||
],
|
||||
"deny": []
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": ["sequential-thinking"],
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-use-tracker.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
# Skills
|
||||
|
||||
Production-tested skills for Claude Code that auto-activate based on context.
|
||||
|
||||
---
|
||||
|
||||
## What Are Skills?
|
||||
|
||||
Skills are modular knowledge bases that Claude loads when needed. They provide:
|
||||
|
||||
- Domain-specific guidelines
|
||||
- Best practices
|
||||
- Code examples
|
||||
- Anti-patterns to avoid
|
||||
|
||||
**Problem:** Skills don't activate automatically by default.
|
||||
|
||||
**Solution:** This showcase includes the hooks + configuration to make them activate.
|
||||
|
||||
---
|
||||
|
||||
## Available Skills
|
||||
|
||||
### skill-developer (Meta-Skill)
|
||||
|
||||
**Purpose:** Creating and managing Claude Code skills
|
||||
|
||||
**Files:** 7 resource files (426 lines total)
|
||||
|
||||
**Use when:**
|
||||
|
||||
- Creating new skills
|
||||
- Understanding skill structure
|
||||
- Working with skill-rules.json
|
||||
- Debugging skill activation
|
||||
|
||||
**Customization:** ✅ None - copy as-is
|
||||
|
||||
**[View Skill →](skill-developer/)**
|
||||
|
||||
---
|
||||
|
||||
### backend-dev-guidelines
|
||||
|
||||
**Purpose:** Node.js/Express/TypeScript development patterns
|
||||
|
||||
**Files:** 12 resource files (304 lines main + resources)
|
||||
|
||||
**Covers:**
|
||||
|
||||
- Layered architecture (Routes → Controllers → Services → Repositories)
|
||||
- BaseController pattern
|
||||
- Prisma database access
|
||||
- Sentry error tracking
|
||||
- Zod validation
|
||||
- UnifiedConfig pattern
|
||||
- Dependency injection
|
||||
- Testing strategies
|
||||
|
||||
**Use when:**
|
||||
|
||||
- Creating/modifying API routes
|
||||
- Building controllers or services
|
||||
- Database operations with Prisma
|
||||
- Setting up error tracking
|
||||
|
||||
**Customization:** ⚠️ Update `pathPatterns` in skill-rules.json to match your backend directories
|
||||
|
||||
**Example pathPatterns:**
|
||||
|
||||
```json
|
||||
{
|
||||
"pathPatterns": [
|
||||
"src/api/**/*.ts", // Single app with src/api
|
||||
"backend/**/*.ts", // Backend directory
|
||||
"services/*/src/**/*.ts" // Multi-service monorepo
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**[View Skill →](backend-dev-guidelines/)**
|
||||
|
||||
---
|
||||
|
||||
## How to Add a Skill to Your Project
|
||||
|
||||
### Quick Integration
|
||||
|
||||
**For Claude Code:**
|
||||
|
||||
```
|
||||
User: "Add the backend-dev-guidelines skill to my project"
|
||||
|
||||
Claude should:
|
||||
1. Ask about project structure
|
||||
2. Copy skill directory
|
||||
3. Update skill-rules.json with their paths
|
||||
4. Verify integration
|
||||
```
|
||||
|
||||
See [CLAUDE_INTEGRATION_GUIDE.md](../../CLAUDE_INTEGRATION_GUIDE.md) for complete instructions.
|
||||
|
||||
### Manual Integration
|
||||
|
||||
**Step 1: Copy the skill directory**
|
||||
|
||||
```bash
|
||||
cp -r claude-code-infrastructure-showcase/.claude/skills/backend-dev-guidelines \\
|
||||
your-project/.claude/skills/
|
||||
```
|
||||
|
||||
**Step 2: Update skill-rules.json**
|
||||
|
||||
If you don't have one, create it:
|
||||
|
||||
```bash
|
||||
cp claude-code-infrastructure-showcase/.claude/skills/skill-rules.json \\
|
||||
your-project/.claude/skills/
|
||||
```
|
||||
|
||||
Then customize the `pathPatterns` for your project:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": {
|
||||
"backend-dev-guidelines": {
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"YOUR_BACKEND_PATH/**/*.ts" // ← Update this!
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Test**
|
||||
|
||||
- Edit a file in your backend directory
|
||||
- The skill should activate automatically
|
||||
|
||||
---
|
||||
|
||||
## skill-rules.json Configuration
|
||||
|
||||
### What It Does
|
||||
|
||||
Defines when skills should activate based on:
|
||||
|
||||
- **Keywords** in user prompts ("backend", "API", "route")
|
||||
- **Intent patterns** (regex matching user intent)
|
||||
- **File path patterns** (editing backend files)
|
||||
- **Content patterns** (code contains Prisma queries)
|
||||
|
||||
### Configuration Format
|
||||
|
||||
```json
|
||||
{
|
||||
"skill-name": {
|
||||
"type": "domain" | "guardrail",
|
||||
"enforcement": "suggest" | "block",
|
||||
"priority": "high" | "medium" | "low",
|
||||
"promptTriggers": {
|
||||
"keywords": ["list", "of", "keywords"],
|
||||
"intentPatterns": ["regex patterns"]
|
||||
},
|
||||
"fileTriggers": {
|
||||
"pathPatterns": ["path/to/files/**/*.ts"],
|
||||
"contentPatterns": ["import.*Prisma"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Enforcement Levels
|
||||
|
||||
- **suggest**: Skill appears as suggestion, doesn't block
|
||||
- **block**: Must use skill before proceeding (guardrail)
|
||||
|
||||
**Use "block" for:**
|
||||
|
||||
- Preventing breaking changes (MUI v6→v7)
|
||||
- Critical database operations
|
||||
- Security-sensitive code
|
||||
|
||||
**Use "suggest" for:**
|
||||
|
||||
- General best practices
|
||||
- Domain guidance
|
||||
- Code organization
|
||||
|
||||
---
|
||||
|
||||
## Creating Your Own Skills
|
||||
|
||||
See the **skill-developer** skill for complete guide on:
|
||||
|
||||
- Skill YAML frontmatter structure
|
||||
- Resource file organization
|
||||
- Trigger pattern design
|
||||
- Testing skill activation
|
||||
|
||||
**Quick template:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: What this skill does
|
||||
---
|
||||
|
||||
# My Skill Title
|
||||
|
||||
## Purpose
|
||||
|
||||
[Why this skill exists]
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
[Auto-activation scenarios]
|
||||
|
||||
## Quick Reference
|
||||
|
||||
[Key patterns and examples]
|
||||
|
||||
## Resource Files
|
||||
|
||||
- [topic-1.md](resources/topic-1.md)
|
||||
- [topic-2.md](resources/topic-2.md)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Skill isn't activating
|
||||
|
||||
**Check:**
|
||||
|
||||
1. Is skill directory in `.claude/skills/`?
|
||||
2. Is skill listed in `skill-rules.json`?
|
||||
3. Do `pathPatterns` match your files?
|
||||
4. Are hooks installed and working?
|
||||
5. Is settings.json configured correctly?
|
||||
|
||||
**Debug:**
|
||||
|
||||
```bash
|
||||
# Check skill exists
|
||||
ls -la .claude/skills/
|
||||
|
||||
# Validate skill-rules.json
|
||||
cat .claude/skills/skill-rules.json | jq .
|
||||
|
||||
# Check hooks are executable
|
||||
ls -la .claude/hooks/*.sh
|
||||
|
||||
# Test hook manually
|
||||
./.claude/hooks/skill-activation-prompt.sh
|
||||
```
|
||||
|
||||
### Skill activates too often
|
||||
|
||||
Update skill-rules.json:
|
||||
|
||||
- Make keywords more specific
|
||||
- Narrow `pathPatterns`
|
||||
- Increase specificity of `intentPatterns`
|
||||
|
||||
### Skill never activates
|
||||
|
||||
Update skill-rules.json:
|
||||
|
||||
- Add more keywords
|
||||
- Broaden `pathPatterns`
|
||||
- Add more `intentPatterns`
|
||||
|
||||
---
|
||||
|
||||
## For Claude Code
|
||||
|
||||
**When integrating a skill for a user:**
|
||||
|
||||
1. **Read [CLAUDE_INTEGRATION_GUIDE.md](../../CLAUDE_INTEGRATION_GUIDE.md)** first
|
||||
2. Ask about their project structure
|
||||
3. Customize `pathPatterns` in skill-rules.json
|
||||
4. Verify the skill file has no hardcoded paths
|
||||
5. Test activation after integration
|
||||
|
||||
**Common mistakes:**
|
||||
|
||||
- Keeping example paths (blog-api/, frontend/)
|
||||
- Not asking about monorepo vs single-app
|
||||
- Copying skill-rules.json without customization
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Start simple:** Add one skill that matches your work
|
||||
2. **Verify activation:** Edit a relevant file, skill should suggest
|
||||
3. **Add more:** Once first skill works, add others
|
||||
4. **Customize:** Adjust triggers based on your workflow
|
||||
|
||||
**Questions?** See [CLAUDE_INTEGRATION_GUIDE.md](../../CLAUDE_INTEGRATION_GUIDE.md) for comprehensive integration instructions.
|
||||
@@ -1,590 +0,0 @@
|
||||
---
|
||||
name: backend-dev-guidelines
|
||||
description: Comprehensive backend development guide for Langfuse's Next.js 14/tRPC/Express/TypeScript monorepo. Use when creating tRPC routers, public API endpoints, BullMQ queue processors, services, or working with tRPC procedures, Next.js API routes, Prisma database access, ClickHouse analytics queries, Redis queues, OpenTelemetry instrumentation, Zod v4 validation, env.mjs configuration, tenant isolation patterns, or async patterns. Covers layered architecture (tRPC procedures → services, queue processors → services), dual database system (PostgreSQL + ClickHouse), projectId filtering for multi-tenant isolation, traceException error handling, observability patterns, and testing strategies (Jest for web, vitest for worker).
|
||||
---
|
||||
|
||||
# Backend Development Guidelines
|
||||
|
||||
## Purpose
|
||||
|
||||
Establish consistency and best practices across Langfuse's backend packages (web, worker, packages/shared) using Next.js 14, tRPC, BullMQ, and TypeScript patterns.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Automatically activates when working on:
|
||||
|
||||
- Creating or modifying tRPC routers and procedures
|
||||
- Creating or modifying public API endpoints (REST)
|
||||
- Creating or modifying BullMQ queue consumers and producers
|
||||
- Building services with business logic
|
||||
- Authenticating API requests
|
||||
- Accessing resources based on entitlements
|
||||
- Implementing middleware (tRPC, NextAuth, public API)
|
||||
- Database operations with Prisma (PostgreSQL) or ClickHouse
|
||||
- Observability with OpenTelemetry, DataDog, logger, and traceException
|
||||
- Input validation with Zod v4
|
||||
- Environment configuration from env variables
|
||||
- Backend testing and refactoring
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### UI: New tRPC Feature Checklist (Web)
|
||||
|
||||
- [ ] **Router**: Define in `features/[feature]/server/*Router.ts`
|
||||
- [ ] **Procedures**: Use appropriate procedure type (protected, public)
|
||||
- [ ] **Authentication**: Use JWT authorization via middlewares.
|
||||
- [ ] **Entitlement check**: Access resources based on resource and role
|
||||
- [ ] **Validation**: Zod v4 schema for input
|
||||
- [ ] **Service**: Business logic in service file
|
||||
- [ ] **Error handling**: Use traceException wrapper
|
||||
- [ ] **Tests**: Unit + integration tests in `__tests__/`
|
||||
- [ ] **Config**: Access via env.mjs
|
||||
|
||||
### SDKs: New Public API Endpoint Checklist (Web)
|
||||
|
||||
- [ ] **Route file**: Create in `pages/api/public/`
|
||||
- [ ] **Wrapper**: Use `withMiddlewares` + `createAuthedProjectAPIRoute`
|
||||
- [ ] **Types**: Define in `features/public-api/types/`
|
||||
- [ ] **Authentication**: Authorization via basic auth
|
||||
- [ ] **Validation**: Zod schemas for query/body/response
|
||||
- [ ] **Versioning**: Versioning in API path and Zod schemas for query/body/response
|
||||
- [ ] **Fern API Docs**: Update `fern/apis/server/definition/` to match TypeScript types
|
||||
- [ ] **Tests**: Add end-to-end test in `__tests__/async/`
|
||||
|
||||
### New Queue Processor Checklist (Worker)
|
||||
|
||||
- [ ] **Processor**: Create in `worker/src/queues/`
|
||||
- [ ] **Queue types**: Create queue types in `packages/shared/src/server/queues`
|
||||
- [ ] **Service**: Business logic in `features/` or `worker/src/features/`
|
||||
- [ ] **Error handling**: Distinguish between errors which should fail queue processing and errors which should result in a succeeded event.
|
||||
- [ ] **Queue registration**: Add to WorkerManager in app.ts
|
||||
- [ ] **Tests**: Add vitest tests in worker
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
```
|
||||
# Web Package (Next.js 14)
|
||||
|
||||
┌─ tRPC API ──────────────────┐ ┌── Public REST API ──────────┐
|
||||
│ │ │ │
|
||||
│ HTTP Request │ │ HTTP Request │
|
||||
│ ↓ │ │ ↓ │
|
||||
│ tRPC Procedure │ │ withMiddlewares + │
|
||||
│ (protectedProjectProcedure)│ │ createAuthedProjectAPIRoute│
|
||||
│ ↓ │ │ ↓ │
|
||||
│ Service (business logic) │ │ Service (business logic) │
|
||||
│ ↓ │ │ ↓ │
|
||||
│ Prisma / ClickHouse │ │ Prisma / ClickHouse │
|
||||
│ │ │ │
|
||||
└─────────────────────────────┘ └─────────────────────────────┘
|
||||
↓
|
||||
[optional]: Publish to Redis BullMQ queue
|
||||
↓
|
||||
┌─ Worker Package (Express) ──────────────────────────────────┐
|
||||
│ │
|
||||
│ BullMQ Queue Job │
|
||||
│ ↓ │
|
||||
│ Queue Processor (handles job) │
|
||||
│ ↓ │
|
||||
│ Service (business logic) │
|
||||
│ ↓ │
|
||||
│ Prisma / ClickHouse │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Principles:**
|
||||
|
||||
- **Web**: tRPC procedures for UI OR public API routes for SDKs → Services → Database
|
||||
- **Worker**: Queue processors → Services → Database
|
||||
- **packages/shared**: Shared code for Web and Worker
|
||||
|
||||
See [architecture-overview.md](architecture-overview.md) for complete details.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Web Package (`/web/`)
|
||||
|
||||
```
|
||||
web/src/
|
||||
├── features/ # Feature-organized code
|
||||
│ ├── [feature-name]/
|
||||
│ │ ├── server/ # Backend logic
|
||||
│ │ │ ├── *Router.ts # tRPC router
|
||||
│ │ │ └── service.ts # Business logic
|
||||
│ │ ├── components/ # React components
|
||||
│ │ └── types/ # Feature types
|
||||
├── server/
|
||||
│ ├── api/
|
||||
│ │ ├── routers/ # tRPC routers
|
||||
│ │ ├── trpc.ts # tRPC setup & middleware
|
||||
│ │ └── root.ts # Main router
|
||||
│ ├── auth.ts # NextAuth.js config
|
||||
│ └── db.ts # Database client
|
||||
├── pages/
|
||||
│ ├── api/
|
||||
│ │ ├── public/ # Public REST APIs
|
||||
│ │ └── trpc/ # tRPC endpoint
|
||||
│ └── [routes].tsx # Next.js pages
|
||||
├── __tests__/ # Jest tests
|
||||
│ └── async/ # Integration tests
|
||||
├── instrumentation.ts # OpenTelemetry (FIRST IMPORT)
|
||||
└── env.mjs # Environment config
|
||||
```
|
||||
|
||||
### Worker Package (`/worker/`)
|
||||
|
||||
```
|
||||
worker/src/
|
||||
├── queues/ # BullMQ processors
|
||||
│ ├── evalQueue.ts
|
||||
│ ├── ingestionQueue.ts
|
||||
│ └── workerManager.ts
|
||||
├── features/ # Business logic
|
||||
│ └── [feature]/
|
||||
│ └── service.ts
|
||||
├── instrumentation.ts # OpenTelemetry (FIRST IMPORT)
|
||||
├── app.ts # Express setup + queue registration
|
||||
├── env.ts # Environment config
|
||||
└── index.ts # Server start
|
||||
```
|
||||
|
||||
### Shared Package (`/packages/shared/`)
|
||||
|
||||
```
|
||||
shared/src/
|
||||
├── server/ # Server utilities
|
||||
│ ├── auth/ # Authentication helpers
|
||||
│ ├── clickhouse/ # ClickHouse client & schema
|
||||
│ ├── instrumentation/ # OpenTelemetry helpers
|
||||
│ ├── llm/ # LLM integration utilities
|
||||
│ ├── redis/ # Redis queues & cache
|
||||
│ ├── repositories/ # Data repositories
|
||||
│ ├── services/ # Shared services
|
||||
│ ├── utils/ # Server utilities
|
||||
│ ├── logger.ts
|
||||
│ └── queues.ts
|
||||
├── encryption/ # Encryption utilities
|
||||
├── features/ # Feature-specific code
|
||||
├── tableDefinitions/ # Table schemas
|
||||
├── utils/ # Shared utilities
|
||||
├── constants.ts
|
||||
├── db.ts # Prisma client
|
||||
├── env.ts # Environment config
|
||||
└── index.ts # Main exports
|
||||
```
|
||||
|
||||
**Import Paths (package.json exports):**
|
||||
|
||||
The shared package exposes specific import paths for different use cases:
|
||||
|
||||
| Import Path | Maps To | Use For |
|
||||
| ------------------------------------------ | --------------------------------- | --------------------------------------------------------------- |
|
||||
| `@langfuse/shared` | `dist/src/index.js` | General types, schemas, utilities, constants |
|
||||
| `@langfuse/shared/src/db` | `dist/src/db.js` | Prisma client and database types |
|
||||
| `@langfuse/shared/src/server` | `dist/src/server/index.js` | Server-side utilities (queues, auth, services, instrumentation) |
|
||||
| `@langfuse/shared/src/server/auth/apiKeys` | `dist/src/server/auth/apiKeys.js` | API key management utilities |
|
||||
| `@langfuse/shared/encryption` | `dist/src/encryption/index.js` | Encryption and signature utilities |
|
||||
|
||||
**Usage Examples:**
|
||||
|
||||
```typescript
|
||||
// General imports - types, schemas, constants, interfaces
|
||||
import {
|
||||
CloudConfigSchema,
|
||||
StringNoHTML,
|
||||
AnnotationQueueObjectType,
|
||||
type APIScoreV2,
|
||||
type ColumnDefinition,
|
||||
Role,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
// Database - Prisma client and types
|
||||
import { prisma, Prisma, JobExecutionStatus } from "@langfuse/shared/src/db";
|
||||
import { type DB as Database } from "@langfuse/shared";
|
||||
|
||||
// Server utilities - queues, services, auth, instrumentation
|
||||
import {
|
||||
logger,
|
||||
instrumentAsync,
|
||||
traceException,
|
||||
redis,
|
||||
getTracesTable,
|
||||
StorageService,
|
||||
sendMembershipInvitationEmail,
|
||||
invalidateApiKeysForProject,
|
||||
recordIncrement,
|
||||
recordHistogram,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
// API key management (specific path)
|
||||
import { createAndAddApiKeysToDb } from "@langfuse/shared/src/server/auth/apiKeys";
|
||||
|
||||
// Encryption utilities
|
||||
import { encrypt, decrypt, sign, verify } from "@langfuse/shared/encryption";
|
||||
```
|
||||
|
||||
**What Goes Where:**
|
||||
|
||||
The shared package provides types, utilities, and server code used by both web and worker packages. It has **5 export paths** that control frontend vs backend access:
|
||||
|
||||
| Import Path | Usage | What's Included |
|
||||
| ------------------------------------------ | --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `@langfuse/shared` | ✅ Frontend + Backend | Prisma types, Zod schemas, constants, table definitions, domain models, utilities |
|
||||
| `@langfuse/shared/src/db` | 🔒 Backend only | Prisma client instance |
|
||||
| `@langfuse/shared/src/server` | 🔒 Backend only | Services, repositories, queues, auth, ClickHouse, LLM integration, instrumentation |
|
||||
| `@langfuse/shared/src/server/auth/apiKeys` | 🔒 Backend only | API key management (separated to avoid circular deps) |
|
||||
| `@langfuse/shared/encryption` | 🔒 Backend only | Database field encryption/decryption |
|
||||
|
||||
**Naming Conventions:**
|
||||
|
||||
- tRPC Routers: `camelCaseRouter.ts` - `datasetRouter.ts`
|
||||
- Services: `service.ts` in feature directory
|
||||
- Queue Processors: `camelCaseQueue.ts` - `evalQueue.ts`
|
||||
- Public APIs: `kebab-case.ts` - `dataset-items.ts`
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. tRPC Procedures Delegate to Services
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER: Business logic in procedures
|
||||
export const traceRouter = createTRPCRouter({
|
||||
byId: protectedProjectProcedure
|
||||
.input(z.object({ traceId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
// 200 lines of logic here
|
||||
}),
|
||||
});
|
||||
|
||||
// ✅ ALWAYS: Delegate to service
|
||||
export const traceRouter = createTRPCRouter({
|
||||
byId: protectedProjectProcedure
|
||||
.input(z.object({ traceId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await getTraceById(input.traceId);
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Access Config via env.mjs, NEVER process.env
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER (except in env.mjs itself)
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
|
||||
// ✅ ALWAYS
|
||||
import { env } from "@/src/env.mjs";
|
||||
const dbUrl = env.DATABASE_URL;
|
||||
```
|
||||
|
||||
### 3. Validate ALL Input with Zod v4
|
||||
|
||||
```typescript
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
projectId: z.string(),
|
||||
});
|
||||
const validated = schema.parse(input);
|
||||
```
|
||||
|
||||
### 4. Services Use Prisma Directly for Simple CRUD or Repositories for Complex Queries
|
||||
|
||||
```typescript
|
||||
// Services use Prisma directly for simple CRUD
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
const dataset = await prisma.dataset.findUnique({
|
||||
where: { id: datasetId, projectId }, // Always filter by projectId for tenant isolation
|
||||
});
|
||||
|
||||
// Or use repositories for complex queries (traces, observations, scores)
|
||||
import { getTracesTable } from "@langfuse/shared/src/server";
|
||||
|
||||
const traces = await getTracesTable({
|
||||
projectId,
|
||||
filter: [...],
|
||||
limit: 1000,
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Observability: OpenTelemetry + DataDog (Not Sentry for Backend)
|
||||
|
||||
**Langfuse uses OpenTelemetry for backend observability, with traces and logs sent to DataDog.**
|
||||
|
||||
```typescript
|
||||
// Import observability utilities
|
||||
import {
|
||||
logger, // Winston logger with OpenTelemetry/DataDog context
|
||||
traceException, // Record exceptions to OpenTelemetry spans
|
||||
instrumentAsync, // Create instrumented spans
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
// Structured logging (includes trace_id, span_id, dd.trace_id)
|
||||
logger.info("Processing dataset", { datasetId, projectId });
|
||||
logger.error("Failed to create dataset", { error: err.message });
|
||||
|
||||
// Record exceptions to OpenTelemetry (sent to DataDog)
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {
|
||||
traceException(error); // Records to current span
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Instrument critical operations (all API routes auto-instrumented)
|
||||
const result = await instrumentAsync(
|
||||
{ name: "dataset.create" },
|
||||
async (span) => {
|
||||
span.setAttributes({ datasetId, projectId });
|
||||
// Operation here
|
||||
return dataset;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
**Note**: Frontend uses Sentry, but backend (tRPC, API routes, services, worker) uses OpenTelemetry + DataDog.
|
||||
|
||||
### 7. Comprehensive Testing Required
|
||||
|
||||
Write tests for all new features and bug fixes. See [testing-guide.md](resources/testing-guide.md) for detailed examples.
|
||||
|
||||
**Test Types:**
|
||||
|
||||
| Type | Framework | Location | Purpose |
|
||||
| ----------- | --------- | --------------------------------------- | ---------------------------- |
|
||||
| Integration | Jest | `web/src/__tests__/async/` | Full API endpoint testing |
|
||||
| tRPC | Jest | `web/src/__tests__/async/` | tRPC procedures with auth |
|
||||
| Service | Jest | `web/src/__tests__/async/repositories/` | Repository/service functions |
|
||||
| Worker | Vitest | `worker/src/__tests__/` | Queue processors & streams |
|
||||
|
||||
**Quick Examples:**
|
||||
|
||||
```typescript
|
||||
// Integration Test (Public API)
|
||||
const res = await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response, "POST", "/api/public/datasets",
|
||||
{ name: "test-dataset" }, auth
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// tRPC Test
|
||||
const { caller } = await prepare(); // Creates session + caller
|
||||
const response = await caller.automations.getAutomations({ projectId });
|
||||
expect(response).toHaveLength(1);
|
||||
|
||||
// Service Test
|
||||
const result = await getObservationsWithModelDataFromEventsTable({
|
||||
projectId, filter: [...], limit: 1000, offset: 0
|
||||
});
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// Worker Test (vitest)
|
||||
const stream = await getObservationStream({ projectId, filter: [] });
|
||||
const rows = [];
|
||||
for await (const chunk of stream) rows.push(chunk);
|
||||
expect(rows).toHaveLength(2);
|
||||
```
|
||||
|
||||
**Key Principles:**
|
||||
|
||||
- Use unique IDs (`randomUUID()`) to avoid test interference
|
||||
- Clean up test data or use unique project IDs
|
||||
- Tests must be independent and runnable in any order
|
||||
- Never use `pruneDatabase` in tests
|
||||
|
||||
### 8. Always Filter by projectId for Tenant Isolation
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: Filter by projectId for tenant isolation
|
||||
const trace = await prisma.trace.findUnique({
|
||||
where: { id: traceId, projectId }, // Required for multi-tenant data isolation
|
||||
});
|
||||
|
||||
// ✅ CORRECT: ClickHouse queries also require projectId
|
||||
const traces = await queryClickhouse({
|
||||
query: `
|
||||
SELECT * FROM traces
|
||||
WHERE project_id = {projectId: String}
|
||||
AND timestamp >= {startTime: DateTime64(3)}
|
||||
`,
|
||||
params: { projectId, startTime },
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Keep Fern API Definitions in Sync with TypeScript Types
|
||||
|
||||
When modifying public API types in `web/src/features/public-api/types/`, the corresponding Fern API definitions in `fern/apis/server/definition/` must be updated to match.
|
||||
|
||||
**Zod to Fern Type Mapping:**
|
||||
|
||||
| Zod Type | Fern Type | Example |
|
||||
| -------- | --------- | ------- |
|
||||
| `.nullish()` | `optional<nullable<T>>` | `z.string().nullish()` → `optional<nullable<string>>` |
|
||||
| `.nullable()` | `nullable<T>` | `z.string().nullable()` → `nullable<string>` |
|
||||
| `.optional()` | `optional<T>` | `z.string().optional()` → `optional<string>` |
|
||||
| Always present | `T` | `z.string()` → `string` |
|
||||
|
||||
**Source References:**
|
||||
|
||||
Add a comment at the top of each Fern type referencing the TypeScript source file:
|
||||
|
||||
```yaml
|
||||
# Source: web/src/features/public-api/types/traces.ts - APITrace
|
||||
Trace:
|
||||
properties:
|
||||
id: string
|
||||
name:
|
||||
type: nullable<string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Imports
|
||||
|
||||
```typescript
|
||||
// tRPC (Web)
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
// Database
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
// ClickHouse
|
||||
import {
|
||||
queryClickhouse,
|
||||
queryClickhouseStream,
|
||||
upsertClickhouse,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
// Observability - OpenTelemetry + DataDog (NOT Sentry for backend)
|
||||
import {
|
||||
logger, // Winston logger with OTEL/DataDog trace context
|
||||
traceException, // Record exceptions to OpenTelemetry spans
|
||||
instrumentAsync, // Create instrumented spans for operations
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
// Config
|
||||
import { env } from "@/src/env.mjs"; // web
|
||||
// or
|
||||
import { env } from "./env"; // worker
|
||||
|
||||
// Public API (Web)
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedProjectAPIRoute } from "@/src/features/public-api/server/createAuthedProjectAPIRoute";
|
||||
|
||||
// Queue Processing (Worker)
|
||||
import { Job } from "bullmq";
|
||||
import { QueueName, TQueueJobTypes } from "@langfuse/shared/src/server";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
| Code | Use Case |
|
||||
| ---- | ------------ |
|
||||
| 200 | Success |
|
||||
| 201 | Created |
|
||||
| 400 | Bad Request |
|
||||
| 401 | Unauthorized |
|
||||
| 403 | Forbidden |
|
||||
| 404 | Not Found |
|
||||
| 500 | Server Error |
|
||||
|
||||
### Example Features to Reference
|
||||
|
||||
Reference existing Langfuse features for implementation patterns:
|
||||
- **Datasets** (`web/src/features/datasets/`) - Complete feature with tRPC router, public API, and service
|
||||
- **Prompts** (`web/src/features/prompts/`) - Feature with versioning and templates
|
||||
- **Evaluations** (`web/src/features/evals/`) - Complex feature with worker integration
|
||||
- **Public API** (`web/src/features/public-api/`) - Middleware and route patterns
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
❌ Business logic in routes/procedures
|
||||
❌ Direct process.env usage (always use env.mjs/env.ts)
|
||||
❌ Missing error handling
|
||||
❌ No input validation (always use Zod v4)
|
||||
❌ Missing projectId filter on tenant-scoped queries
|
||||
❌ console.log instead of logger/traceException (OpenTelemetry)
|
||||
|
||||
---
|
||||
|
||||
## Navigation Guide
|
||||
|
||||
| Need to... | Read this |
|
||||
| ------------------------- | ------------------------------------------------------------ |
|
||||
| Understand architecture | [architecture-overview.md](resources/architecture-overview.md) |
|
||||
| Create routes/controllers | [routing-and-controllers.md](resources/routing-and-controllers.md) |
|
||||
| Organize business logic | [services-and-repositories.md](resources/services-and-repositories.md) |
|
||||
| Create middleware | [middleware-guide.md](resources/middleware-guide.md) |
|
||||
| Database access | [database-patterns.md](resources/database-patterns.md) |
|
||||
| Manage config | [configuration.md](resources/configuration.md) |
|
||||
| Write tests | [testing-guide.md](resources/testing-guide.md) |
|
||||
|
||||
---
|
||||
|
||||
## Resource Files
|
||||
|
||||
### [architecture-overview.md](resources/architecture-overview.md)
|
||||
|
||||
Three-layer architecture (tRPC/Public API → Services → Data Access), request lifecycle for tRPC/Public API/Worker, Next.js 14 directory structure, dual database system (PostgreSQL + ClickHouse), separation of concerns, repository pattern for complex queries
|
||||
|
||||
### [routing-and-controllers.md](resources/routing-and-controllers.md)
|
||||
|
||||
Next.js file-based routing, tRPC router patterns, Public REST API routes, layered architecture (Entry Points → Services → Repositories → Database), service layer organization, anti-patterns to avoid
|
||||
|
||||
### [services-and-repositories.md](resources/services-and-repositories.md)
|
||||
|
||||
Service layer overview, dependency injection patterns, singleton patterns, repository pattern for data access, service design principles, caching strategies, testing services
|
||||
|
||||
### [middleware-guide.md](resources/middleware-guide.md)
|
||||
|
||||
tRPC middleware (withErrorHandling, withOtelInstrumentation, enforceUserIsAuthed), seven tRPC procedure types (publicProcedure, authenticatedProcedure, protectedProjectProcedure, etc.), Public API middleware (withMiddlewares, createAuthedProjectAPIRoute), authentication patterns (NextAuth for tRPC, Basic Auth for Public API)
|
||||
|
||||
### [database-patterns.md](resources/database-patterns.md)
|
||||
|
||||
Dual database architecture (PostgreSQL via Prisma + ClickHouse via direct client), PostgreSQL CRUD operations, ClickHouse query patterns (queryClickhouse, queryClickhouseStream, upsertClickhouse), repository pattern for complex queries, tenant isolation with projectId filtering, when to use which database
|
||||
|
||||
### [configuration.md](resources/configuration.md)
|
||||
|
||||
Environment variable validation with Zod, package-specific configs (web/env.mjs with t3-oss/env-nextjs, worker/env.ts, shared/env.ts), NEXT_PUBLIC_LANGFUSE_CLOUD_REGION usage, LANGFUSE_EE_LICENSE_KEY for enterprise features, best practices for env management
|
||||
|
||||
### [testing-guide.md](resources/testing-guide.md)
|
||||
|
||||
Integration tests (Public API with makeZodVerifiedAPICall), tRPC tests (createInnerTRPCContext, appRouter.createCaller), service-level tests (repository/service functions), worker tests (vitest with streams), test isolation principles, running tests (Jest for web, vitest for worker)
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **database-verification** - Verify column names and schema consistency
|
||||
- **skill-developer** - Meta-skill for creating and managing skills
|
||||
|
||||
---
|
||||
|
||||
**Skill Status**: COMPLETE ✅
|
||||
**Line Count**: ~540 lines
|
||||
**Progressive Disclosure**: 7 resource files ✅
|
||||
@@ -1,870 +0,0 @@
|
||||
# Architecture Overview - Langfuse Backend
|
||||
|
||||
Complete guide to the layered architecture pattern used in Langfuse's Next.js 14/tRPC/Express monorepo.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Layered Architecture Pattern](#layered-architecture-pattern)
|
||||
- [Request Lifecycle](#request-lifecycle)
|
||||
- [Directory Structure](#directory-structure)
|
||||
- [Module Organization](#module-organization)
|
||||
- [Separation of Concerns](#separation-of-concerns)
|
||||
- [Database Architecture](#database-architecture)
|
||||
|
||||
---
|
||||
|
||||
## Layered Architecture Pattern
|
||||
|
||||
Langfuse uses a **three-layer architecture** with two primary entry points (tRPC and Public API) plus async processing via Worker.
|
||||
|
||||
### The Three Layers
|
||||
|
||||
```
|
||||
# Web Package (Next.js 14)
|
||||
|
||||
┌─ tRPC API ──────────────────┐ ┌── Public REST API ──────────┐
|
||||
│ │ │ │
|
||||
│ HTTP Request │ │ HTTP Request │
|
||||
│ ↓ │ │ ↓ │
|
||||
│ tRPC Procedure │ │ withMiddlewares + │
|
||||
│ (protectedProjectProcedure)│ │ createAuthedProjectAPIRoute│
|
||||
│ ↓ │ │ ↓ │
|
||||
│ Service (business logic) │ │ Service (business logic) │
|
||||
│ ↓ │ │ ↓ │
|
||||
│ Prisma / ClickHouse │ │ Prisma / ClickHouse │
|
||||
│ │ │ │
|
||||
└─────────────────────────────┘ └─────────────────────────────┘
|
||||
↓
|
||||
[optional]: Publish to Redis BullMQ queue
|
||||
↓
|
||||
┌─ Worker Package (Express) ──────────────────────────────────┐
|
||||
│ │
|
||||
│ BullMQ Queue Job │
|
||||
│ ↓ │
|
||||
│ Queue Processor (handles job) │
|
||||
│ ↓ │
|
||||
│ Service (business logic) │
|
||||
│ ↓ │
|
||||
│ Prisma / ClickHouse │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layer Breakdown
|
||||
|
||||
**Layer 1: API Entry Points**
|
||||
|
||||
Two types of entry points:
|
||||
|
||||
- **tRPC Procedures** - Type-safe RPC for UI
|
||||
- Located in `features/[feature]/server/*Router.ts`
|
||||
- Uses middleware for auth/validation
|
||||
- Types shared between client/server
|
||||
|
||||
- **Public REST APIs** - REST endpoints for SDKs
|
||||
- Located in `pages/api/public/`
|
||||
- Uses `withMiddlewares` + `createAuthedProjectAPIRoute`
|
||||
- Versioned with Zod schemas
|
||||
|
||||
**Layer 2: Services**
|
||||
|
||||
- Business logic and orchestration
|
||||
- Shared between tRPC, Public API, and Worker
|
||||
- Located in `features/[feature]/server/service.ts`
|
||||
- No HTTP/Request/Response knowledge
|
||||
- Use repositories for complex queries or Prisma directly for simple CRUD
|
||||
|
||||
**Layer 3: Data Access**
|
||||
|
||||
- **Repositories** for complex data access patterns (traces, observations, scores, events)
|
||||
- **Direct Prisma** for simple CRUD operations in services
|
||||
- PostgreSQL for transactional data
|
||||
- ClickHouse for analytics/traces (accessed via repositories)
|
||||
- Redis for caching/queues
|
||||
|
||||
**Async Processing Layer: Worker**
|
||||
|
||||
- BullMQ queue processors
|
||||
- Same service layer as Web
|
||||
- Handles long-running operations
|
||||
|
||||
### Why This Architecture?
|
||||
|
||||
**Testability:**
|
||||
|
||||
- tRPC procedures easily testable with type-safe callers
|
||||
- Services tested independently with mocked DB
|
||||
- Queue processors tested with vitest
|
||||
- Clear test boundaries
|
||||
|
||||
**Maintainability:**
|
||||
|
||||
- Business logic isolated in services
|
||||
- tRPC provides type safety end-to-end
|
||||
- Changes to API don't affect service layer
|
||||
- Easy to locate and fix bugs
|
||||
|
||||
**Reusability:**
|
||||
|
||||
- Services used by tRPC, Public API, Worker, and scripts
|
||||
- Business logic not tied to HTTP or tRPC
|
||||
- Consistent patterns across packages
|
||||
|
||||
**Scalability:**
|
||||
|
||||
- Worker handles async operations separately
|
||||
- Easy to add new tRPC procedures
|
||||
- Clear patterns to follow
|
||||
- Shared code in packages/shared
|
||||
|
||||
---
|
||||
|
||||
## Request Lifecycle
|
||||
|
||||
### tRPC Request Flow (UI)
|
||||
|
||||
```typescript
|
||||
1. HTTP POST /api/trpc/datasets.create
|
||||
↓
|
||||
2. Next.js API route catches request (pages/api/trpc/[trpc].ts)
|
||||
↓
|
||||
3. tRPC router resolves procedure:
|
||||
- Match route to procedure in datasetRouter.ts
|
||||
↓
|
||||
4. tRPC middleware chain executes:
|
||||
- protectedProjectProcedure (authentication)
|
||||
- hasEntitlement checks
|
||||
- Input validation with Zod v4
|
||||
↓
|
||||
5. Procedure handler calls service:
|
||||
export const datasetRouter = createTRPCRouter({
|
||||
create: protectedProjectProcedure
|
||||
.input(createDatasetSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return await createDataset(input, ctx.session);
|
||||
}),
|
||||
})
|
||||
↓
|
||||
6. Service executes business logic:
|
||||
- Validate business rules
|
||||
- Use repositories for complex queries or Prisma directly
|
||||
- ClickHouse queries via repositories if needed
|
||||
↓
|
||||
7. Database operations:
|
||||
- prisma.dataset.create({ data })
|
||||
- clickhouse queries via getTracesTable()
|
||||
↓
|
||||
8. Response flows back:
|
||||
Database → Service → Procedure → tRPC → Client
|
||||
```
|
||||
|
||||
### Public API Request Flow (SDKs)
|
||||
|
||||
```typescript
|
||||
1. HTTP POST /api/public/datasets
|
||||
↓
|
||||
2. Next.js API route handler (pages/api/public/datasets.ts)
|
||||
↓
|
||||
3. withMiddlewares wrapper executes:
|
||||
- Basic auth verification
|
||||
- Rate limiting
|
||||
- CORS handling
|
||||
↓
|
||||
4. createAuthedProjectAPIRoute handler:
|
||||
- Parse and validate request with Zod v4
|
||||
- Extract auth context (project, user)
|
||||
↓
|
||||
5. Handler calls service function:
|
||||
const dataset = await createDataset({
|
||||
name: req.body.name,
|
||||
projectId: req.auth.projectId,
|
||||
});
|
||||
↓
|
||||
6. Service executes (same as tRPC path)
|
||||
↓
|
||||
7. Response formatted and returned:
|
||||
res.status(201).json(dataset);
|
||||
```
|
||||
|
||||
### Worker/Queue Processing Flow
|
||||
|
||||
```typescript
|
||||
1. Job added to Redis BullMQ queue:
|
||||
await evalQueue.add("eval-job", {
|
||||
evalId, projectId
|
||||
});
|
||||
↓
|
||||
2. Worker picks up job from Redis
|
||||
↓
|
||||
3. Queue processor handles job:
|
||||
// worker/src/queues/evalQueue.ts
|
||||
async process(job: Job<EvalJobType>) {
|
||||
await processEvaluation(job.data);
|
||||
}
|
||||
↓
|
||||
4. Processor calls service:
|
||||
- Same service layer as Web
|
||||
- Business logic execution
|
||||
↓
|
||||
5. Service performs operations:
|
||||
- Prisma transactions
|
||||
- ClickHouse queries
|
||||
- External API calls (LLMs)
|
||||
↓
|
||||
6. Job completes or fails:
|
||||
- Success: job.updateProgress(100)
|
||||
- Failure: throw error for retry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Web Package (`/web/src/`)
|
||||
|
||||
```
|
||||
web/src/
|
||||
├── features/ # Feature-organized code
|
||||
│ ├── datasets/
|
||||
│ │ ├── server/ # Backend logic
|
||||
│ │ │ ├── datasetRouter.ts # tRPC router
|
||||
│ │ │ └── datasetService.ts # Business logic
|
||||
│ │ ├── components/ # React components
|
||||
│ │ └── types/ # Feature types
|
||||
│ │
|
||||
│ ├── public-api/
|
||||
│ │ ├── server/
|
||||
│ │ │ ├── withMiddlewares.ts
|
||||
│ │ │ └── createAuthedProjectAPIRoute.ts
|
||||
│ │ └── types/ # API schemas
|
||||
│ │
|
||||
│ └── [feature-name]/
|
||||
│ ├── server/
|
||||
│ │ ├── *Router.ts # tRPC router
|
||||
│ │ └── service.ts # Business logic
|
||||
│ ├── components/
|
||||
│ └── types/
|
||||
│
|
||||
├── server/
|
||||
│ ├── api/
|
||||
│ │ ├── routers/ # tRPC routers
|
||||
│ │ ├── trpc.ts # tRPC setup & middleware
|
||||
│ │ └── root.ts # Main router combining all
|
||||
│ ├── auth.ts # NextAuth.js config
|
||||
│ └── db.ts # Database utilities
|
||||
│
|
||||
├── pages/
|
||||
│ ├── api/
|
||||
│ │ ├── public/ # Public REST APIs
|
||||
│ │ │ ├── datasets.ts
|
||||
│ │ │ └── traces.ts
|
||||
│ │ └── trpc/
|
||||
│ │ └── [trpc].ts # tRPC endpoint
|
||||
│ └── [routes].tsx # Next.js pages
|
||||
│
|
||||
├── __tests__/ # Jest tests
|
||||
│ ├── async/ # Integration tests
|
||||
│ └── sync/ # Unit tests
|
||||
│
|
||||
├── instrumentation.ts # OpenTelemetry (FIRST IMPORT)
|
||||
└── env.mjs # Environment config
|
||||
```
|
||||
|
||||
### Worker Package (`/worker/src/`)
|
||||
|
||||
```
|
||||
worker/src/
|
||||
├── queues/ # BullMQ processors
|
||||
│ ├── evalQueue.ts # Evaluation jobs
|
||||
│ ├── ingestionQueue.ts # Data ingestion
|
||||
│ ├── batchExportQueue.ts # Batch exports
|
||||
│ └── workerManager.ts # Queue registration
|
||||
│
|
||||
├── features/ # Business logic
|
||||
│ └── [feature]/
|
||||
│ └── service.ts
|
||||
│
|
||||
├── __tests__/ # Vitest tests
|
||||
│
|
||||
├── instrumentation.ts # OpenTelemetry (FIRST IMPORT)
|
||||
├── app.ts # Express setup + queue registration
|
||||
├── env.ts # Environment config
|
||||
└── index.ts # Server start
|
||||
```
|
||||
|
||||
### Shared Package (`/packages/shared/`)
|
||||
|
||||
The shared package provides types, utilities, and server code used by both web and worker packages. It has **5 export paths** that control frontend vs backend access:
|
||||
|
||||
| Import Path | Usage | What's Included |
|
||||
| ------------------------------------------ | --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `@langfuse/shared` | ✅ Frontend + Backend | Prisma types, Zod schemas, constants, table definitions, domain models, utilities |
|
||||
| `@langfuse/shared/src/db` | 🔒 Backend only | Prisma client instance |
|
||||
| `@langfuse/shared/src/server` | 🔒 Backend only | Services, repositories, queues, auth, ClickHouse, LLM integration, instrumentation |
|
||||
| `@langfuse/shared/src/server/auth/apiKeys` | 🔒 Backend only | API key management (separated to avoid circular deps) |
|
||||
| `@langfuse/shared/encryption` | 🔒 Backend only | Database field encryption/decryption |
|
||||
|
||||
**Key Structure:**
|
||||
|
||||
```
|
||||
packages/shared/src/
|
||||
├── server/ # 🔒 All server-only code
|
||||
│ ├── auth/ # Authentication & authorization
|
||||
│ ├── clickhouse/ # ClickHouse client & queries
|
||||
│ ├── redis/ # Redis client & 30+ queue types
|
||||
│ ├── repositories/ # Data access (traces, observations, scores, events)
|
||||
│ ├── services/ # Business services (Storage, Email, Slack, etc.)
|
||||
│ ├── llm/ # LLM integration
|
||||
│ ├── instrumentation/ # OpenTelemetry
|
||||
│ └── queues.ts, logger.ts, filterToPrisma.ts, etc.
|
||||
│
|
||||
├── features/ # ✅ Feature types (evals, scores, prompts, datasets)
|
||||
├── domain/ # ✅ Domain models (automations, webhooks, etc.)
|
||||
├── tableDefinitions/ # ✅ Table schemas
|
||||
├── interfaces/ # ✅ Shared interfaces (filters, orderBy)
|
||||
├── utils/ # ✅ Utilities (JSON, Zod, string checks)
|
||||
├── encryption/ # 🔒 Encryption utilities
|
||||
└── db.ts, constants.ts, types.ts, etc.
|
||||
```
|
||||
|
||||
**Common Import Patterns:**
|
||||
|
||||
```typescript
|
||||
// ✅ Main export - Safe for frontend + backend
|
||||
import {
|
||||
Prisma,
|
||||
Role,
|
||||
type Dataset,
|
||||
CloudConfigSchema,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
// 🔒 Database - Backend only
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
// 🔒 Server utilities - Backend only
|
||||
import {
|
||||
logger,
|
||||
instrumentAsync,
|
||||
traceException,
|
||||
redis,
|
||||
clickhouseClient,
|
||||
StorageService,
|
||||
fetchLLMCompletion,
|
||||
filterToPrisma,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
// 🔒 API keys - Backend only
|
||||
import { createAndAddApiKeysToDb } from "@langfuse/shared/src/server/auth/apiKeys";
|
||||
|
||||
// 🔒 Encryption - Backend only
|
||||
import { encrypt, decrypt } from "@langfuse/shared/encryption";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Organization
|
||||
|
||||
### Feature-Based Organization (Recommended)
|
||||
|
||||
For most features, organize by domain within `features/`:
|
||||
|
||||
```
|
||||
src/features/datasets/
|
||||
├── server/ # Backend code
|
||||
│ ├── datasetRouter.ts # tRPC procedures
|
||||
│ └── service.ts # Business logic
|
||||
├── components/ # React components
|
||||
│ ├── DatasetTable.tsx
|
||||
│ └── DatasetForm.tsx
|
||||
├── types/ # Feature types
|
||||
│ └── index.ts
|
||||
└── utils/ # Feature utilities
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Any feature with UI + API
|
||||
- Clear domain boundary
|
||||
- Multiple related procedures
|
||||
|
||||
### Subdomain Organization
|
||||
|
||||
For complex features with multiple subdomains:
|
||||
|
||||
```
|
||||
src/features/evaluations/
|
||||
├── server/
|
||||
│ ├── evalRouter.ts # Main router
|
||||
│ ├── evalService.ts # Core service
|
||||
│ ├── templates/ # Template subdomain
|
||||
│ │ ├── templateRouter.ts
|
||||
│ │ └── templateService.ts
|
||||
│ └── configs/ # Config subdomain
|
||||
│ ├── configRouter.ts
|
||||
│ └── configService.ts
|
||||
├── components/
|
||||
│ ├── templates/
|
||||
│ └── configs/
|
||||
└── types/
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Feature has 10+ files
|
||||
- Clear subdomains exist
|
||||
- Logical grouping improves clarity
|
||||
|
||||
### Flat Organization (Rare)
|
||||
|
||||
For small, standalone features:
|
||||
|
||||
```
|
||||
src/server/api/routers/
|
||||
├── healthRouter.ts # Simple health check
|
||||
└── versionRouter.ts # Version info
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Simple features (1-2 procedures)
|
||||
- No UI components
|
||||
- Standalone utilities
|
||||
|
||||
---
|
||||
|
||||
## Separation of Concerns
|
||||
|
||||
### What Goes Where
|
||||
|
||||
**tRPC Procedures (Entry Layer):**
|
||||
|
||||
- ✅ Procedure definitions (query/mutation)
|
||||
- ✅ Middleware application (auth, validation)
|
||||
- ✅ Input schemas (Zod v4)
|
||||
- ✅ Service delegation
|
||||
- ✅ Error transformation (TRPCError)
|
||||
- ❌ Business logic (belongs in services)
|
||||
- ❌ Database operations (belongs in services)
|
||||
- ❌ Complex validation (belongs in services)
|
||||
|
||||
**Public API Routes (Entry Layer):**
|
||||
|
||||
- ✅ Route registration
|
||||
- ✅ Middleware wrapper application
|
||||
- ✅ Input validation (Zod v4)
|
||||
- ✅ Service delegation
|
||||
- ✅ Response formatting
|
||||
- ✅ HTTP status codes
|
||||
- ❌ Business logic (belongs in services)
|
||||
- ❌ Database operations (belongs in services)
|
||||
|
||||
**Services Layer:**
|
||||
|
||||
- ✅ Business logic
|
||||
- ✅ Business rules enforcement
|
||||
- ✅ Transaction orchestration
|
||||
- ✅ Repository calls for complex queries
|
||||
- ✅ Direct Prisma operations for simple CRUD
|
||||
- ✅ ClickHouse queries (via repositories)
|
||||
- ✅ Redis cache access
|
||||
- ✅ External API calls (LLMs, etc.)
|
||||
- ❌ HTTP concerns (Request/Response)
|
||||
- ❌ tRPC-specific types (TRPCError in entry layer)
|
||||
- ❌ NextAuth session handling (passed as parameter)
|
||||
|
||||
**Queue Processors (Worker):**
|
||||
|
||||
- ✅ Job registration and configuration
|
||||
- ✅ Job data extraction
|
||||
- ✅ Service delegation
|
||||
- ✅ Progress updates
|
||||
- ✅ Error handling (retry logic)
|
||||
- ❌ Business logic (belongs in services)
|
||||
- ❌ Database operations (belongs in services)
|
||||
|
||||
### Example: Dataset Creation
|
||||
|
||||
**tRPC Procedure (Entry Point):**
|
||||
|
||||
```typescript
|
||||
// web/src/features/datasets/server/datasetRouter.ts
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createDataset } from "./service";
|
||||
|
||||
export const datasetRouter = createTRPCRouter({
|
||||
create: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createDataset({
|
||||
...input,
|
||||
userId: ctx.session.user.id,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to create dataset",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
**Service (Business Logic):**
|
||||
|
||||
```typescript
|
||||
// web/src/features/datasets/server/service.ts
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { instrumentAsync, traceException } from "@langfuse/shared/src/server";
|
||||
|
||||
export async function createDataset(data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
projectId: string;
|
||||
userId: string;
|
||||
}) {
|
||||
return await instrumentAsync({ name: "dataset.create" }, async (span) => {
|
||||
// Business rule: Check for duplicate names in project
|
||||
const existing = await prisma.dataset.findFirst({
|
||||
where: {
|
||||
name: data.name,
|
||||
projectId: data.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error(`Dataset with name "${data.name}" already exists`);
|
||||
}
|
||||
|
||||
// Create dataset
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
projectId: data.projectId,
|
||||
createdById: data.userId,
|
||||
},
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
datasetId: dataset.id,
|
||||
projectId: dataset.projectId,
|
||||
});
|
||||
|
||||
return dataset;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Public API (Alternative Entry Point):**
|
||||
|
||||
```typescript
|
||||
// web/src/pages/api/public/datasets.ts
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedProjectAPIRoute } from "@/src/features/public-api/server/createAuthedProjectAPIRoute";
|
||||
import { createDataset } from "@/src/features/datasets/server/service";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const createDatasetSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export default withMiddlewares({
|
||||
POST: createAuthedProjectAPIRoute({
|
||||
name: "Create Dataset",
|
||||
bodySchema: createDatasetSchema,
|
||||
fn: async ({ body, auth, res }) => {
|
||||
const dataset = await createDataset({
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
projectId: auth.scope.projectId,
|
||||
userId: auth.scope.userId,
|
||||
});
|
||||
|
||||
return res.status(201).json(dataset);
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
**Queue Processor (Async Processing):**
|
||||
|
||||
```typescript
|
||||
// worker/src/queues/datasetExportQueue.ts
|
||||
import { Job } from "bullmq";
|
||||
import { exportDataset } from "../features/datasets/exportService";
|
||||
|
||||
export async function processDatasetExport(
|
||||
job: Job<{ datasetId: string; projectId: string; format: string }>,
|
||||
) {
|
||||
const { datasetId, projectId, format } = job.data;
|
||||
|
||||
await job.updateProgress(10);
|
||||
|
||||
// Delegate to service
|
||||
const exportUrl = await exportDataset({
|
||||
datasetId,
|
||||
projectId,
|
||||
format,
|
||||
onProgress: (percent) => job.updateProgress(percent),
|
||||
});
|
||||
|
||||
await job.updateProgress(100);
|
||||
|
||||
return { exportUrl };
|
||||
}
|
||||
```
|
||||
|
||||
**Notice:** Each layer has clear, distinct responsibilities!
|
||||
|
||||
- **Entry layers** (tRPC/Public API/Queue) handle protocol concerns
|
||||
- **Service layer** contains all business logic
|
||||
- **Data layer** accessed via repositories (complex queries) or Prisma directly (simple CRUD)
|
||||
|
||||
---
|
||||
|
||||
## Database Architecture
|
||||
|
||||
### Dual Database System
|
||||
|
||||
Langfuse uses two databases with different purposes:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ PostgreSQL │ │ ClickHouse │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Transactional│ │ Analytics │ │
|
||||
│ │ Data │ │ Data │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ ↑ ↑ │
|
||||
│ │ │ │
|
||||
│ Prisma ORM Direct SQL │
|
||||
│ (schema migrations) (via client) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**PostgreSQL (Primary Database):**
|
||||
|
||||
- Accessed via Prisma ORM
|
||||
- Transactional data (users, projects, datasets, etc.)
|
||||
- ACID guarantees
|
||||
- Schema managed via `prisma migrate`
|
||||
- Located in `packages/shared/prisma/`
|
||||
|
||||
**ClickHouse (Analytics Database):**
|
||||
|
||||
- Accessed via direct SQL queries
|
||||
- High-volume trace/observation data
|
||||
- Columnar storage for analytics
|
||||
- Optimized for aggregations
|
||||
- Schema in `packages/shared/src/server/clickhouse/`
|
||||
- Schema managed via `golang-migrate`
|
||||
|
||||
**Redis (Cache & Queues):**
|
||||
|
||||
- BullMQ job queues
|
||||
- Caching layer
|
||||
- Session storage
|
||||
- Rate limiting
|
||||
|
||||
### Data Access Pattern
|
||||
|
||||
**Services access databases directly:**
|
||||
|
||||
```typescript
|
||||
// PostgreSQL via Prisma
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
const dataset = await prisma.dataset.create({ data });
|
||||
|
||||
// ClickHouse via helper functions
|
||||
import { getTracesTable } from "@langfuse/shared/src/server";
|
||||
|
||||
const traces = await getTracesTable({
|
||||
projectId,
|
||||
filter: [...],
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
// Redis via queue/cache utilities
|
||||
import { redis } from "@langfuse/shared/src/server";
|
||||
|
||||
await redis.set(`cache:${key}`, value, "EX", 3600);
|
||||
```
|
||||
|
||||
**Repository Pattern:**
|
||||
|
||||
Langfuse uses repositories in `packages/shared/src/server/repositories/` for complex data access patterns. Repositories provide:
|
||||
|
||||
- Abstraction over complex queries (traces, observations, scores, events)
|
||||
- Data converters for transforming database models to application models
|
||||
- ClickHouse query builders and stream processing
|
||||
- Reusable query logic across services
|
||||
|
||||
Services can use repositories for complex operations OR Prisma directly for simple CRUD operations.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Keep Procedures Thin
|
||||
|
||||
tRPC procedures should only handle protocol concerns:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Business logic in procedure
|
||||
export const datasetRouter = createTRPCRouter({
|
||||
create: protectedProjectProcedure
|
||||
.input(createSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// 200 lines of business logic here
|
||||
const existing = await prisma.dataset.findFirst(...);
|
||||
if (existing) throw new Error(...);
|
||||
const dataset = await prisma.dataset.create(...);
|
||||
await sendNotification(...);
|
||||
return dataset;
|
||||
}),
|
||||
});
|
||||
|
||||
// ✅ GOOD: Delegate to service
|
||||
export const datasetRouter = createTRPCRouter({
|
||||
create: protectedProjectProcedure
|
||||
.input(createSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return await createDataset(input, ctx.session);
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Services Should Be Protocol-Agnostic
|
||||
|
||||
Services should work regardless of entry point:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: No HTTP/tRPC knowledge
|
||||
export async function createDataset(data: CreateDatasetInput) {
|
||||
// Pure business logic
|
||||
return await prisma.dataset.create({ data });
|
||||
}
|
||||
|
||||
// ❌ BAD: tRPC-specific
|
||||
export async function createDataset(ctx: TRPCContext) {
|
||||
// Coupled to tRPC
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Observability with OpenTelemetry + DataDog
|
||||
|
||||
**Langfuse uses OpenTelemetry for backend observability, with traces and logs sent to DataDog.**
|
||||
|
||||
Use structured logging and instrumentation:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
logger,
|
||||
traceException,
|
||||
instrumentAsync,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
export async function processEvaluation(evalId: string) {
|
||||
return await instrumentAsync(
|
||||
{ name: "evaluation.process", attributes: { evalId } },
|
||||
async (span) => {
|
||||
// Structured logging (includes trace_id, span_id, dd.trace_id)
|
||||
logger.info("Starting evaluation", { evalId });
|
||||
|
||||
try {
|
||||
// Operation here
|
||||
const result = await runEvaluation(evalId);
|
||||
|
||||
span.setAttributes({
|
||||
score: result.score,
|
||||
status: "success",
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Record exception to OpenTelemetry span (sent to DataDog)
|
||||
traceException(error, span);
|
||||
logger.error("Evaluation failed", { evalId, error: error.message });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Frontend uses Sentry for error tracking, but backend (tRPC, API routes, services, worker) uses OpenTelemetry + DataDog.
|
||||
|
||||
### 4. Use Proper Error Handling
|
||||
|
||||
Transform errors at entry points:
|
||||
|
||||
```typescript
|
||||
// tRPC procedure
|
||||
try {
|
||||
return await service();
|
||||
} catch (error) {
|
||||
traceException(error); // Record to OpenTelemetry span (sent to DataDog)
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "User-friendly message",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
// Public API
|
||||
try {
|
||||
return await service();
|
||||
} catch (error) {
|
||||
traceException(error); // Record to OpenTelemetry span (sent to DataDog)
|
||||
return res.status(500).json({
|
||||
error: "User-friendly message",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Validate at Entry Points
|
||||
|
||||
Use Zod v4 for all input validation:
|
||||
|
||||
```typescript
|
||||
import { z } from "zod/v4";
|
||||
|
||||
// tRPC
|
||||
.input(z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
projectId: z.string(),
|
||||
}))
|
||||
|
||||
// Public API
|
||||
const bodySchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
});
|
||||
const validated = bodySchema.parse(req.body);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
|
||||
- [SKILL.md](../SKILL.md) - Main guide
|
||||
- [routing-and-controllers.md](routing-and-controllers.md) - tRPC and Public API details
|
||||
- [services-and-repositories.md](services-and-repositories.md) - Service patterns
|
||||
- [testing-guide.md](testing-guide.md) - Testing strategies
|
||||
@@ -1,562 +0,0 @@
|
||||
# Configuration Management - Environment Variables
|
||||
|
||||
Complete guide to managing configuration across Langfuse's monorepo packages.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Environment Variable Pattern](#environment-variable-pattern)
|
||||
- [Package-Specific Configuration](#package-specific-configuration)
|
||||
- [Special Environment Variables](#special-environment-variables)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## Environment Variable Pattern
|
||||
|
||||
### Why Zod-Validated Environment Variables?
|
||||
|
||||
**Problems with raw process.env:**
|
||||
|
||||
- ❌ No type safety
|
||||
- ❌ No validation
|
||||
- ❌ Hard to test
|
||||
- ❌ Runtime errors for typos
|
||||
- ❌ No default values
|
||||
|
||||
**Benefits of Zod validation:**
|
||||
|
||||
- ✅ Type-safe configuration
|
||||
- ✅ Validated at startup
|
||||
- ✅ Clear error messages
|
||||
- ✅ Default values
|
||||
- ✅ Environment-specific transformation
|
||||
|
||||
---
|
||||
|
||||
## Package-Specific Configuration
|
||||
|
||||
Each package has its own `env.ts` or `env.mjs` file that validates and exports environment variables:
|
||||
|
||||
```
|
||||
langfuse/
|
||||
├── web/src/env.mjs # Next.js app (t3-env pattern)
|
||||
├── worker/src/env.ts # Worker service (Zod schema)
|
||||
├── packages/shared/src/env.ts # Shared config (Zod schema)
|
||||
└── ee/src/env.ts # Enterprise Edition (Zod schema)
|
||||
```
|
||||
|
||||
### Web Package (`web/src/env.mjs`)
|
||||
|
||||
Uses **t3-oss/env-nextjs** for Next.js-specific validation with server/client separation.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Separates server-side and client-side environment variables
|
||||
- Client variables must be prefixed with `NEXT_PUBLIC_`
|
||||
- Validates at build time (unless `DOCKER_BUILD=1`)
|
||||
- `runtimeEnv` section manually maps all variables
|
||||
|
||||
**Structure:**
|
||||
|
||||
```typescript
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
export const env = createEnv({
|
||||
// Server-side only variables (never exposed to client)
|
||||
server: {
|
||||
DATABASE_URL: z.string().url(),
|
||||
NEXTAUTH_SECRET: z.string().min(1),
|
||||
SALT: z.string(),
|
||||
CLICKHOUSE_URL: z.string().url(),
|
||||
// ... 100+ server variables
|
||||
},
|
||||
|
||||
// Client-side variables (exposed to browser)
|
||||
client: {
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z
|
||||
.enum(["US", "EU", "STAGING", "DEV", "HIPAA"])
|
||||
.optional(),
|
||||
NEXT_PUBLIC_SIGN_UP_DISABLED: z.enum(["true", "false"]).default("false"),
|
||||
// ... client variables
|
||||
},
|
||||
|
||||
// Runtime mapping (required for Next.js edge runtime)
|
||||
runtimeEnv: {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION:
|
||||
process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
|
||||
// ... must map ALL variables
|
||||
},
|
||||
|
||||
// Skip validation in Docker builds
|
||||
skipValidation: process.env.DOCKER_BUILD === "1",
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// In server-side code (tRPC, API routes)
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
const dbUrl = env.DATABASE_URL;
|
||||
const salt = env.SALT;
|
||||
|
||||
// In client-side code (React components)
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
const region = env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION;
|
||||
```
|
||||
|
||||
### Worker Package (`worker/src/env.ts`)
|
||||
|
||||
Uses **plain Zod schema** for Express.js worker service.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```typescript
|
||||
import { z } from "zod/v4";
|
||||
import { removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
BUILD_ID: z.string().optional(),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
DATABASE_URL: z.string(),
|
||||
PORT: z.coerce.number().positive().max(65536).default(3030),
|
||||
|
||||
// ClickHouse
|
||||
CLICKHOUSE_URL: z.string().url(),
|
||||
CLICKHOUSE_USER: z.string(),
|
||||
CLICKHOUSE_PASSWORD: z.string(),
|
||||
|
||||
// S3 Event Upload (required)
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string({
|
||||
error: "Langfuse requires a bucket name for S3 Event Uploads.",
|
||||
}),
|
||||
|
||||
// Queue concurrency settings
|
||||
LANGFUSE_INGESTION_QUEUE_PROCESSING_CONCURRENCY: z.coerce
|
||||
.number()
|
||||
.positive()
|
||||
.default(20),
|
||||
LANGFUSE_EVAL_EXECUTION_WORKER_CONCURRENCY: z.coerce
|
||||
.number()
|
||||
.positive()
|
||||
.default(5),
|
||||
|
||||
// Queue consumer toggles
|
||||
QUEUE_CONSUMER_INGESTION_QUEUE_IS_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
QUEUE_CONSUMER_BATCH_EXPORT_QUEUE_IS_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
|
||||
// ... 150+ worker-specific variables
|
||||
});
|
||||
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
process.env.DOCKER_BUILD === "1"
|
||||
? (process.env as any)
|
||||
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { env } from "./env";
|
||||
|
||||
const concurrency = env.LANGFUSE_INGESTION_QUEUE_PROCESSING_CONCURRENCY;
|
||||
const s3Bucket = env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET;
|
||||
```
|
||||
|
||||
### Shared Package (`packages/shared/src/env.ts`)
|
||||
|
||||
Uses **plain Zod schema** for configuration shared between web and worker.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```typescript
|
||||
import { z } from "zod/v4";
|
||||
import { removeEmptyEnvVariables } from "./utils/environment";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
|
||||
// Redis configuration
|
||||
REDIS_HOST: z.string().nullish(),
|
||||
REDIS_PORT: z.coerce.number().positive().max(65536).default(6379).nullable(),
|
||||
REDIS_AUTH: z.string().nullish(),
|
||||
REDIS_CONNECTION_STRING: z.string().nullish(),
|
||||
REDIS_CLUSTER_ENABLED: z.enum(["true", "false"]).default("false"),
|
||||
|
||||
// ClickHouse
|
||||
CLICKHOUSE_URL: z.string().url(),
|
||||
CLICKHOUSE_USER: z.string(),
|
||||
CLICKHOUSE_PASSWORD: z.string(),
|
||||
CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(25),
|
||||
|
||||
// S3 Event Upload
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string(),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: z.string().optional(),
|
||||
|
||||
// Logging
|
||||
LANGFUSE_LOG_LEVEL: z
|
||||
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
|
||||
.optional(),
|
||||
LANGFUSE_LOG_FORMAT: z.enum(["text", "json"]).default("text"),
|
||||
|
||||
// Encryption
|
||||
ENCRYPTION_KEY: z
|
||||
.string()
|
||||
.length(
|
||||
64,
|
||||
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32",
|
||||
)
|
||||
.optional(),
|
||||
|
||||
// ... 80+ shared variables
|
||||
});
|
||||
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
process.env.DOCKER_BUILD === "1"
|
||||
? (process.env as any)
|
||||
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { env } from "@langfuse/shared/src/env";
|
||||
|
||||
const redisHost = env.REDIS_HOST;
|
||||
const clickhouseUrl = env.CLICKHOUSE_URL;
|
||||
```
|
||||
|
||||
### Enterprise Edition Package (`ee/src/env.ts`)
|
||||
|
||||
Minimal Zod schema for EE-specific variables.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```typescript
|
||||
import { z } from "zod/v4";
|
||||
import { removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
|
||||
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { env } from "@langfuse/ee/src/env";
|
||||
|
||||
const licenseKey = env.LANGFUSE_EE_LICENSE_KEY;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Special Environment Variables
|
||||
|
||||
### NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
|
||||
**Purpose:** Identifies the cloud deployment region for Langfuse Cloud.
|
||||
|
||||
**Type:** `"US" | "EU" | "STAGING" | "DEV" | "HIPAA" | undefined`
|
||||
|
||||
**Where Used:**
|
||||
|
||||
- **web/src/env.mjs** - Client-side accessible (prefixed with `NEXT_PUBLIC_`)
|
||||
- **ee/src/env.ts** - Enterprise features
|
||||
- **packages/shared/src/env.ts** - Shared logic
|
||||
- **worker/src/env.ts** - Worker processing
|
||||
|
||||
**When Set:**
|
||||
|
||||
| Environment | Value | Purpose |
|
||||
| ------------------------ | ---------------------- | ---------------------------------------------- |
|
||||
| **Developer Laptop** | `"DEV"` or `"STAGING"` | Local development against cloud infrastructure |
|
||||
| **Langfuse Cloud US** | `"US"` | Production US region |
|
||||
| **Langfuse Cloud EU** | `"EU"` | Production EU region |
|
||||
| **Langfuse Cloud HIPAA** | `"HIPAA"` | HIPAA-compliant region |
|
||||
| **OSS Self-Hosted** | `undefined` (not set) | Self-hosted deployments don't have region |
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
```typescript
|
||||
// Check if running in cloud
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
|
||||
// Enable cloud-specific features
|
||||
- Usage metering and billing
|
||||
- Cloud spend alerts
|
||||
- Free tier enforcement
|
||||
- Stripe integration
|
||||
- PostHog analytics
|
||||
}
|
||||
|
||||
// Region-specific behavior
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "HIPAA") {
|
||||
// HIPAA compliance features
|
||||
}
|
||||
|
||||
// Development/staging checks
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV") {
|
||||
// Enable debug features
|
||||
}
|
||||
```
|
||||
|
||||
**Example Configuration:**
|
||||
|
||||
```bash
|
||||
# .env file on developer laptop
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=DEV
|
||||
|
||||
# Cloud US deployment
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=US
|
||||
|
||||
# Self-hosted OSS deployment
|
||||
# (variable not set)
|
||||
```
|
||||
|
||||
### LANGFUSE_EE_LICENSE_KEY
|
||||
|
||||
**Purpose:** Enables Enterprise Edition features in self-hosted deployments.
|
||||
|
||||
**Type:** `string | undefined`
|
||||
|
||||
**Where Used:**
|
||||
|
||||
- **web/src/env.mjs** - Web app EE features
|
||||
- **ee/src/env.ts** - EE package
|
||||
|
||||
**When Set:**
|
||||
|
||||
| Deployment | Value | Features Enabled |
|
||||
| ------------------- | ------------------ | ---------------------------------------------------------------- |
|
||||
| **Langfuse Cloud** | Not set | Cloud features controlled by `NEXT_PUBLIC_LANGFUSE_CLOUD_REGION` |
|
||||
| **OSS Self-Hosted** | Not set | Core open-source features only |
|
||||
| **EE Self-Hosted** | License key string | Enterprise features enabled |
|
||||
|
||||
**Enterprise Features Controlled:**
|
||||
|
||||
When `LANGFUSE_EE_LICENSE_KEY` is set and valid:
|
||||
|
||||
- SSO integrations (custom OIDC, SAML)
|
||||
- Advanced RBAC
|
||||
- Audit logging
|
||||
- Custom branding
|
||||
- SLA support
|
||||
- Advanced security features
|
||||
|
||||
**Usage Pattern:**
|
||||
|
||||
```typescript
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
// Check if EE license is present
|
||||
if (env.LANGFUSE_EE_LICENSE_KEY) {
|
||||
// Validate license
|
||||
const isValidLicense = await validateEELicense(env.LANGFUSE_EE_LICENSE_KEY);
|
||||
|
||||
if (isValidLicense) {
|
||||
// Enable EE features
|
||||
enableCustomSSO();
|
||||
enableAdvancedRBAC();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example Configuration:**
|
||||
|
||||
```bash
|
||||
# OSS self-hosted (no license)
|
||||
# LANGFUSE_EE_LICENSE_KEY not set
|
||||
|
||||
# EE self-hosted
|
||||
LANGFUSE_EE_LICENSE_KEY=ee_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Langfuse Cloud (uses region instead)
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=US
|
||||
# LANGFUSE_EE_LICENSE_KEY not used
|
||||
```
|
||||
|
||||
### Other Important Variables
|
||||
|
||||
**DOCKER_BUILD**
|
||||
|
||||
```typescript
|
||||
// Skip validation during Docker builds
|
||||
skipValidation: process.env.DOCKER_BUILD === "1";
|
||||
```
|
||||
|
||||
**Purpose:** Docker builds happen before runtime env vars are available, so validation must be skipped.
|
||||
|
||||
**SALT**
|
||||
|
||||
```typescript
|
||||
SALT: z.string({
|
||||
required_error: "A strong Salt is required to encrypt API keys securely.",
|
||||
});
|
||||
```
|
||||
|
||||
**Purpose:** Required for encrypting API keys in database. Must be set in production.
|
||||
|
||||
**ENCRYPTION_KEY**
|
||||
|
||||
```typescript
|
||||
ENCRYPTION_KEY: z.string().length(64, "Must be 256 bits, 64 hex characters");
|
||||
```
|
||||
|
||||
**Purpose:** Optional 256-bit key for encrypting sensitive database fields.
|
||||
|
||||
**Generate:** `openssl rand -hex 32`
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Import from env.mjs/env.ts
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER DO THIS
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
|
||||
// ✅ ALWAYS DO THIS
|
||||
import { env } from "@/src/env.mjs";
|
||||
const dbUrl = env.DATABASE_URL; // Type-safe, validated
|
||||
```
|
||||
|
||||
### 2. Use Appropriate Import Path
|
||||
|
||||
```typescript
|
||||
// In web package
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
// In worker package
|
||||
import { env } from "./env";
|
||||
|
||||
// In shared package
|
||||
import { env } from "@langfuse/shared/src/env";
|
||||
```
|
||||
|
||||
### 3. Client Variables Must Start with NEXT*PUBLIC*
|
||||
|
||||
```typescript
|
||||
// ❌ Won't work in browser
|
||||
API_KEY: z.string(); // in server config
|
||||
|
||||
// ✅ Accessible in browser
|
||||
NEXT_PUBLIC_API_KEY: z.string(); // in client config
|
||||
```
|
||||
|
||||
### 4. Provide Sensible Defaults for Development
|
||||
|
||||
```typescript
|
||||
PORT: z.coerce.number().positive().default(3030),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
REDIS_PORT: z.coerce.number().positive().default(6379),
|
||||
```
|
||||
|
||||
### 5. Use Coercion for Numbers
|
||||
|
||||
```typescript
|
||||
// .env files are always strings
|
||||
PORT: z.coerce.number(); // Converts "3000" to 3000
|
||||
```
|
||||
|
||||
### 6. Transform Complex Values
|
||||
|
||||
```typescript
|
||||
// Split comma-separated values
|
||||
LANGFUSE_LOG_PROPAGATED_HEADERS: z.string().optional().transform((s) =>
|
||||
s ? s.split(",").map((s) => s.toLowerCase().trim()) : []
|
||||
),
|
||||
|
||||
// Parse project:rate pairs
|
||||
LANGFUSE_INGESTION_PROCESSING_SAMPLED_PROJECTS: z.string().optional().transform((val) => {
|
||||
const map = new Map<string, number>();
|
||||
val?.split(",").forEach(part => {
|
||||
const [projectId, rate] = part.split(":");
|
||||
map.set(projectId, parseFloat(rate));
|
||||
});
|
||||
return map;
|
||||
}),
|
||||
```
|
||||
|
||||
### 7. Validation at Startup
|
||||
|
||||
All environment variables are validated when the application starts. Invalid configuration will cause immediate failure with clear error messages:
|
||||
|
||||
```bash
|
||||
❌ Validation error:
|
||||
- SALT: Required
|
||||
- CLICKHOUSE_URL: Invalid url
|
||||
- PORT: Number must be less than or equal to 65536
|
||||
```
|
||||
|
||||
### 8. Skip Validation in Docker Builds
|
||||
|
||||
Always include the Docker build escape hatch:
|
||||
|
||||
```typescript
|
||||
export const env =
|
||||
process.env.DOCKER_BUILD === "1"
|
||||
? (process.env as any)
|
||||
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
```
|
||||
|
||||
### 9. Use removeEmptyEnvVariables Helper
|
||||
|
||||
Treats empty strings as undefined:
|
||||
|
||||
```typescript
|
||||
import { removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
|
||||
EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
```
|
||||
|
||||
This prevents errors from `.env` files with empty values:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
OPTIONAL_VAR= # Treated as undefined, not empty string
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration File Locations
|
||||
|
||||
```
|
||||
langfuse/
|
||||
├── .env # Local development overrides
|
||||
├── .env.dev.example # Example dev configuration
|
||||
├── web/src/env.mjs # Web app env validation
|
||||
├── worker/src/env.ts # Worker env validation
|
||||
├── packages/shared/src/env.ts # Shared env validation
|
||||
└── ee/src/env.ts # EE env validation
|
||||
```
|
||||
|
||||
**DO NOT commit:**
|
||||
|
||||
- `.env`
|
||||
- `.env.local`
|
||||
- `.env.production`
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
|
||||
- [SKILL.md](../SKILL.md) - Main guide
|
||||
- [architecture-overview.md](architecture-overview.md) - Architecture patterns
|
||||
@@ -1,660 +0,0 @@
|
||||
# Database Patterns - PostgreSQL & ClickHouse
|
||||
|
||||
Complete guide to database access patterns in Langfuse using PostgreSQL (Prisma ORM) and ClickHouse (direct client).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Database Architecture Overview](#database-architecture-overview)
|
||||
- [PostgreSQL with Prisma](#postgresql-with-prisma)
|
||||
- [ClickHouse with Direct Client](#clickhouse-with-direct-client)
|
||||
- [Repository Pattern](#repository-pattern)
|
||||
- [When to Use Which Database](#when-to-use-which-database)
|
||||
- [Error Handling](#error-handling)
|
||||
|
||||
---
|
||||
|
||||
## Database Architecture Overview
|
||||
|
||||
Langfuse uses a **dual database architecture**:
|
||||
|
||||
| Database | Technology | Purpose | Access Pattern |
|
||||
| -------------- | ----------------- | ------------------------------------------------------------- | -------------------------------------- |
|
||||
| **PostgreSQL** | Prisma ORM | Transactional data, relational data, CRUD operations | Type-safe ORM with migrations |
|
||||
| **ClickHouse** | Direct SQL client | Analytics data, high-volume traces/observations, aggregations | Raw SQL queries with streaming support |
|
||||
| **Redis** | ioredis | Queues (BullMQ), caching, rate limiting | Direct client access |
|
||||
|
||||
**Key Principle**: Use PostgreSQL for transactional data and relationships. Use ClickHouse for high-volume analytics and time-series data.
|
||||
|
||||
**⚠️ Important**: All queries must filter by `project_id` (or `projectId`) to ensure proper data isolation between tenants. This is essential for the multi-tenant architecture.
|
||||
|
||||
---
|
||||
|
||||
## PostgreSQL with Prisma
|
||||
|
||||
### Import Pattern
|
||||
|
||||
```typescript
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
// Direct access to Prisma client
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
```
|
||||
|
||||
**Important**: Always import from `@langfuse/shared/src/db`, not `@prisma/client` directly.
|
||||
|
||||
### Common CRUD Operations
|
||||
|
||||
**⚠️ ALWAYS include `projectId` in WHERE clauses** for project-scoped data:
|
||||
|
||||
```typescript
|
||||
// Create
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
name: "My Project",
|
||||
orgId: organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ GOOD: Read with projectId filter
|
||||
const trace = await prisma.trace.findUnique({
|
||||
where: { id: traceId, projectId }, // ← Always include projectId for tenant isolation
|
||||
include: {
|
||||
scores: true,
|
||||
project: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// ❌ BAD: Missing projectId filter
|
||||
// const trace = await prisma.trace.findUnique({
|
||||
// where: { id: traceId }, // ← Missing projectId!
|
||||
// });
|
||||
|
||||
// Update
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { lastLogin: new Date() },
|
||||
});
|
||||
|
||||
// ✅ GOOD: Delete with projectId
|
||||
await prisma.apiKey.delete({
|
||||
where: { id: apiKeyId, projectId }, // ← Always include projectId
|
||||
});
|
||||
|
||||
// ✅ GOOD: Count with projectId
|
||||
const traceCount = await prisma.trace.count({
|
||||
where: { projectId, userId }, // ← Always include projectId
|
||||
});
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
Use Prisma interactive transactions for operations that must be atomic:
|
||||
|
||||
```typescript
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({ data: userData });
|
||||
|
||||
const project = await tx.project.create({
|
||||
data: {
|
||||
name: "Default Project",
|
||||
orgId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.projectMembership.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
projectId: project.id,
|
||||
role: "OWNER",
|
||||
},
|
||||
});
|
||||
|
||||
return { user, project };
|
||||
});
|
||||
```
|
||||
|
||||
**Transaction options:**
|
||||
|
||||
```typescript
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
// Transaction logic
|
||||
},
|
||||
{
|
||||
maxWait: 5000, // Max time to wait for transaction to start (ms)
|
||||
timeout: 10000, // Max time transaction can run (ms)
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Query Optimization
|
||||
|
||||
**Use `select` to limit fields:**
|
||||
|
||||
```typescript
|
||||
// ❌ Fetches all fields (including large JSON columns)
|
||||
const traces = await prisma.trace.findMany({ where: { projectId } });
|
||||
|
||||
// ✅ Only fetch needed fields
|
||||
const traces = await prisma.trace.findMany({
|
||||
where: { projectId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
timestamp: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Prevent N+1 queries with `include`:**
|
||||
|
||||
```typescript
|
||||
// ❌ N+1 Query Problem
|
||||
const projects = await prisma.project.findMany();
|
||||
for (const project of projects) {
|
||||
// N additional queries
|
||||
const memberCount = await prisma.projectMembership.count({
|
||||
where: { projectId: project.id },
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Use include or aggregation
|
||||
const projects = await prisma.project.findMany({
|
||||
include: {
|
||||
members: { select: { userId: true, role: true } },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Pagination:**
|
||||
|
||||
```typescript
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const traces = await prisma.trace.findMany({
|
||||
where: { projectId },
|
||||
orderBy: { timestamp: "desc" },
|
||||
take: PAGE_SIZE,
|
||||
skip: page * PAGE_SIZE,
|
||||
});
|
||||
```
|
||||
|
||||
## ClickHouse with Direct Client
|
||||
|
||||
### Import Pattern
|
||||
|
||||
```typescript
|
||||
import { queryClickhouse } from "@langfuse/shared/src/server/repositories/clickhouse";
|
||||
import { clickhouseClient } from "@langfuse/shared/src/server/clickhouse/client";
|
||||
```
|
||||
|
||||
### ClickHouse Client Singleton
|
||||
|
||||
ClickHouse uses a singleton client manager that reuses connections:
|
||||
|
||||
```typescript
|
||||
import { clickhouseClient } from "@langfuse/shared/src/server/clickhouse/client";
|
||||
|
||||
// Get client (automatically reuses existing connection)
|
||||
const client = clickhouseClient();
|
||||
|
||||
// For read-only queries (uses read replica if configured)
|
||||
const client = clickhouseClient(undefined, "ReadOnly");
|
||||
```
|
||||
|
||||
### Query Patterns
|
||||
|
||||
ClickHouse queries use **raw SQL** with parameterized queries. Parameters use `{paramName: Type}` syntax:
|
||||
|
||||
**⚠️ Important**: All ClickHouse queries must include `project_id` filter to ensure proper tenant isolation.
|
||||
|
||||
**Simple query:**
|
||||
|
||||
```typescript
|
||||
import { queryClickhouse } from "@langfuse/shared/src/server/repositories/clickhouse";
|
||||
|
||||
// ✅ GOOD: Always filter by project_id
|
||||
const rows = await queryClickhouse<{ id: string; name: string }>({
|
||||
query: `
|
||||
SELECT id, name, timestamp
|
||||
FROM traces
|
||||
WHERE project_id = {projectId: String} -- ← REQUIRED: Always filter by project_id
|
||||
AND timestamp >= {startTime: DateTime64(3)}
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT {limit: UInt32}
|
||||
`,
|
||||
params: {
|
||||
projectId, // ← Required for tenant isolation
|
||||
startTime: convertDateToClickhouseDateTime(startDate),
|
||||
limit: 100,
|
||||
},
|
||||
tags: { feature: "tracing", type: "trace" },
|
||||
});
|
||||
|
||||
// ❌ BAD: Missing project_id filter
|
||||
// const rows = await queryClickhouse({
|
||||
// query: `SELECT * FROM traces WHERE timestamp >= {startTime: DateTime64(3)}`,
|
||||
// params: { startTime },
|
||||
// });
|
||||
```
|
||||
|
||||
**Streaming query (for large result sets):**
|
||||
|
||||
```typescript
|
||||
import { queryClickhouseStream } from "@langfuse/shared/src/server/repositories/clickhouse";
|
||||
|
||||
// Stream results to avoid loading all rows in memory
|
||||
for await (const row of queryClickhouseStream<ObservationRecordReadType>({
|
||||
query: `
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE project_id = {projectId: String}
|
||||
AND start_time >= {startTime: DateTime64(3)}
|
||||
`,
|
||||
params: { projectId, startTime },
|
||||
})) {
|
||||
// Process row by row
|
||||
await processObservation(row);
|
||||
}
|
||||
```
|
||||
|
||||
**Upsert (insert) operation:**
|
||||
|
||||
```typescript
|
||||
import { upsertClickhouse } from "@langfuse/shared/src/server/repositories/clickhouse";
|
||||
|
||||
await upsertClickhouse({
|
||||
table: "traces",
|
||||
records: [
|
||||
{
|
||||
id: traceId,
|
||||
project_id: projectId,
|
||||
timestamp: new Date(),
|
||||
name: "API Call",
|
||||
user_id: userId,
|
||||
// ... other fields
|
||||
},
|
||||
],
|
||||
eventBodyMapper: (record) => ({
|
||||
// Transform record for event log
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
// ... other fields
|
||||
}),
|
||||
tags: { feature: "ingestion", type: "trace" },
|
||||
});
|
||||
```
|
||||
|
||||
**DDL/Administrative commands:**
|
||||
|
||||
```typescript
|
||||
import { commandClickhouse } from "@langfuse/shared/src/server/repositories/clickhouse";
|
||||
|
||||
// Create table, alter schema, etc.
|
||||
await commandClickhouse({
|
||||
query: `
|
||||
ALTER TABLE traces
|
||||
ADD COLUMN IF NOT EXISTS new_field String
|
||||
`,
|
||||
tags: { feature: "migration" },
|
||||
});
|
||||
```
|
||||
|
||||
### ClickHouse Type Mapping
|
||||
|
||||
| JavaScript Type | ClickHouse Param Type |
|
||||
| --------------- | --------------------------------------------------------- |
|
||||
| `string` | `String` |
|
||||
| `number` | `UInt32`, `Int64`, `Float64` |
|
||||
| `Date` | `DateTime64(3)` (use `convertDateToClickhouseDateTime()`) |
|
||||
| `boolean` | `UInt8` (0 or 1) |
|
||||
| `string[]` | `Array(String)` |
|
||||
|
||||
**Date handling:**
|
||||
|
||||
```typescript
|
||||
import { convertDateToClickhouseDateTime } from "@langfuse/shared/src/server/clickhouse/client";
|
||||
|
||||
const params = {
|
||||
startTime: convertDateToClickhouseDateTime(new Date()),
|
||||
};
|
||||
```
|
||||
|
||||
### ClickHouse Query Best Practices
|
||||
|
||||
**1. Always filter by `project_id` for tenant isolation:**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: project_id filter is required
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM traces
|
||||
WHERE project_id = {projectId: String} -- ← Required for tenant isolation
|
||||
AND timestamp >= {startTime: DateTime64(3)}
|
||||
`;
|
||||
|
||||
// ❌ WRONG: Missing project_id filter
|
||||
// const query = `
|
||||
// SELECT * FROM traces WHERE timestamp >= {startTime: DateTime64(3)}
|
||||
// `;
|
||||
```
|
||||
|
||||
**Why this is important:**
|
||||
- Langfuse is multi-tenant - each project's data must be isolated
|
||||
- The `project_id` filter ensures queries only access data from the intended tenant
|
||||
- All queries on project-scoped tables (traces, observations, scores, sessions, etc.) must filter by `project_id`
|
||||
|
||||
**2. Use LIMIT BY for deduplication:**
|
||||
|
||||
```typescript
|
||||
// Get latest version of each trace
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM traces
|
||||
WHERE project_id = {projectId: String} -- ← Always include project_id
|
||||
ORDER BY event_ts DESC
|
||||
LIMIT 1 BY id, project_id
|
||||
`;
|
||||
```
|
||||
|
||||
**3. Use time-based filtering for performance:**
|
||||
|
||||
```typescript
|
||||
// Combine project_id filter with timestamp for optimal performance
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE project_id = {projectId: String} -- ← Required for tenant isolation
|
||||
AND start_time >= {startTime: DateTime64(3)} -- ← Improves performance
|
||||
AND start_time < {endTime: DateTime64(3)}
|
||||
`;
|
||||
```
|
||||
|
||||
**4. Use CTEs for complex queries (still require `project_id`):**
|
||||
|
||||
```typescript
|
||||
const query = `
|
||||
WITH observations_agg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
count() as observation_count,
|
||||
sum(total_cost) as total_cost
|
||||
FROM observations
|
||||
WHERE project_id = {projectId: String} -- ← Filter in CTE
|
||||
GROUP BY trace_id
|
||||
)
|
||||
SELECT
|
||||
t.id,
|
||||
t.name,
|
||||
o.observation_count,
|
||||
o.total_cost
|
||||
FROM traces t
|
||||
LEFT JOIN observations_agg o ON t.id = o.trace_id
|
||||
WHERE t.project_id = {projectId: String} -- ← Filter in main query
|
||||
`;
|
||||
```
|
||||
|
||||
**Note**: When using CTEs or subqueries, ensure `project_id` filter is applied at each level.
|
||||
|
||||
**Error handling with retries:**
|
||||
|
||||
ClickHouse queries automatically retry on network errors (socket hang up). Custom error handling for resource limits:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
queryClickhouse,
|
||||
ClickHouseResourceError,
|
||||
} from "@langfuse/shared/src/server/repositories/clickhouse";
|
||||
|
||||
try {
|
||||
const rows = await queryClickhouse({ query, params });
|
||||
} catch (error) {
|
||||
if (error instanceof ClickHouseResourceError) {
|
||||
// Memory limit, timeout, or overcommit error
|
||||
throw new Error(ClickHouseResourceError.ERROR_ADVICE_MESSAGE);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
Langfuse uses repositories in `packages/shared/src/server/repositories/` for complex data access patterns.
|
||||
|
||||
### When to Use Repositories
|
||||
|
||||
✅ **Use repositories when:**
|
||||
|
||||
- Complex ClickHouse queries with CTEs, aggregations, or joins
|
||||
- Query used in multiple places (DRY principle)
|
||||
- Need data transformation/converters (DB → domain models)
|
||||
- Building reusable query logic with filters
|
||||
|
||||
❌ **Use direct Prisma/ClickHouse for:**
|
||||
|
||||
- Simple CRUD operations
|
||||
- One-off queries
|
||||
- Prototyping (refactor to repository later)
|
||||
|
||||
### Repository Examples
|
||||
|
||||
**Trace repository (ClickHouse):**
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/server/repositories/traces.ts
|
||||
export const getTracesByIds = async (
|
||||
projectId: string,
|
||||
traceIds: string[],
|
||||
): Promise<TraceRecordReadType[]> => {
|
||||
const rows = await queryClickhouse<TraceRecordReadType>({
|
||||
query: `
|
||||
SELECT *
|
||||
FROM traces
|
||||
WHERE project_id = {projectId: String}
|
||||
AND id IN ({traceIds: Array(String)})
|
||||
ORDER BY event_ts DESC
|
||||
LIMIT 1 BY id, project_id
|
||||
`,
|
||||
params: { projectId, traceIds },
|
||||
tags: { feature: "tracing", type: "trace" },
|
||||
});
|
||||
|
||||
return rows.map(convertClickhouseToDomain);
|
||||
};
|
||||
```
|
||||
|
||||
**Score repository (PostgreSQL + ClickHouse):**
|
||||
|
||||
```typescript
|
||||
// Repositories can query both databases
|
||||
export const getScoresByTraceId = async (
|
||||
projectId: string,
|
||||
traceId: string,
|
||||
) => {
|
||||
// Use ClickHouse for analytics
|
||||
const clickhouseScores = await queryClickhouse<ScoreRecordReadType>({
|
||||
query: `
|
||||
SELECT *
|
||||
FROM scores
|
||||
WHERE project_id = {projectId: String}
|
||||
AND trace_id = {traceId: String}
|
||||
`,
|
||||
params: { projectId, traceId },
|
||||
});
|
||||
|
||||
// Use Prisma for config data
|
||||
const scoreConfigs = await prisma.scoreConfig.findMany({
|
||||
where: { projectId },
|
||||
});
|
||||
|
||||
return enrichScoresWithConfigs(clickhouseScores, scoreConfigs);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Which Database
|
||||
|
||||
| Use Case | Database | Reasoning |
|
||||
| -------------------------------------- | ---------- | ------------------------------------------ |
|
||||
| User accounts, projects, API keys | PostgreSQL | Transactional data with strong consistency |
|
||||
| Prompt management, dataset definitions | PostgreSQL | Configuration data with relations |
|
||||
| Project settings, RBAC permissions | PostgreSQL | Small, frequently updated data |
|
||||
| Traces, observations, events | ClickHouse | High-volume time-series data |
|
||||
| Score aggregations, analytics queries | ClickHouse | Fast aggregations over millions of rows |
|
||||
| Usage metrics, cost calculations | ClickHouse | Analytical queries with GROUP BY |
|
||||
| Exports, large dataset queries | ClickHouse | Streaming support for large result sets |
|
||||
|
||||
**Decision flow:**
|
||||
|
||||
1. Is it high-volume time-series data? → **ClickHouse**
|
||||
2. Does it need aggregation over millions of rows? → **ClickHouse**
|
||||
3. Is it transactional data with relationships? → **PostgreSQL**
|
||||
4. Is it configuration or user data? → **PostgreSQL**
|
||||
5. Is it frequently updated? → **PostgreSQL**
|
||||
6. Is it append-only analytics data? → **ClickHouse**
|
||||
|
||||
### Project-Scoped vs Global Tables
|
||||
|
||||
**Project-scoped tables (MUST filter by `project_id`):**
|
||||
- `traces` - All trace queries require `project_id`
|
||||
- `observations` - All observation queries require `project_id`
|
||||
- `scores` - All score queries require `project_id`
|
||||
- `events` - All event queries require `project_id`
|
||||
- `dataset_run_items_rmt` - All dataset run queries require `project_id`
|
||||
|
||||
**Global tables (no `project_id` filter needed):**
|
||||
- `users` - User management (use `id` for filtering)
|
||||
- `organizations` - Organization data (use `id` for filtering)
|
||||
- System configuration tables
|
||||
|
||||
**Example of correct filtering:**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: Project-scoped query
|
||||
const traces = await queryClickhouse({
|
||||
query: `
|
||||
SELECT * FROM traces
|
||||
WHERE project_id = {projectId: String}
|
||||
AND timestamp >= {startTime: DateTime64(3)}
|
||||
`,
|
||||
params: { projectId, startTime },
|
||||
});
|
||||
|
||||
// ✅ CORRECT: Global table query (no project_id needed)
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
// ❌ WRONG: Project-scoped query without project_id filter
|
||||
// const traces = await queryClickhouse({
|
||||
// query: `SELECT * FROM traces WHERE timestamp >= {startTime: DateTime64(3)}`,
|
||||
// });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### PostgreSQL (Prisma) Errors
|
||||
|
||||
```typescript
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
try {
|
||||
await prisma.user.create({ data: userData });
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
// Unique constraint violation
|
||||
if (error.code === "P2002") {
|
||||
const target = error.meta?.target as string[];
|
||||
throw new ConflictError(`${target?.join(", ")} already exists`);
|
||||
}
|
||||
|
||||
// Foreign key constraint
|
||||
if (error.code === "P2003") {
|
||||
throw new ValidationError("Invalid reference");
|
||||
}
|
||||
|
||||
// Record not found
|
||||
if (error.code === "P2025") {
|
||||
throw new NotFoundError("Record not found");
|
||||
}
|
||||
|
||||
// Record required to connect not found
|
||||
if (error.code === "P2018") {
|
||||
throw new ValidationError("Related record not found");
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown error
|
||||
logger.error("Prisma error", { error });
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
**Common Prisma error codes:**
|
||||
|
||||
| Code | Meaning | Typical Cause |
|
||||
| -------- | ---------------------------- | ------------------------------------- |
|
||||
| `P2002` | Unique constraint violation | Duplicate email, API key, etc. |
|
||||
| `P2003` | Foreign key constraint | Referenced record doesn't exist |
|
||||
| `P2025` | Record not found | Update/delete of non-existent record |
|
||||
| `P2018` | Required relation not found | Connect to non-existent related record |
|
||||
|
||||
### ClickHouse Errors
|
||||
|
||||
```typescript
|
||||
import {
|
||||
queryClickhouse,
|
||||
ClickHouseResourceError,
|
||||
} from "@langfuse/shared/src/server/repositories/clickhouse";
|
||||
|
||||
try {
|
||||
const rows = await queryClickhouse({ query, params });
|
||||
} catch (error) {
|
||||
// ClickHouse resource errors (memory limit, timeout, overcommit)
|
||||
if (error instanceof ClickHouseResourceError) {
|
||||
logger.warn("ClickHouse resource error", {
|
||||
errorType: error.errorType, // "MEMORY_LIMIT" | "OVERCOMMIT" | "TIMEOUT"
|
||||
message: error.message,
|
||||
});
|
||||
|
||||
// User-friendly error message
|
||||
throw new BadRequestError(ClickHouseResourceError.ERROR_ADVICE_MESSAGE);
|
||||
}
|
||||
|
||||
// Network/connection errors are automatically retried
|
||||
logger.error("ClickHouse error", { error });
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
**ClickHouse error types:**
|
||||
|
||||
| Error Type | Discriminator | Meaning | Solution |
|
||||
| --------------- | ----------------------- | ---------------------------- | -------------------------------------------------- |
|
||||
| `MEMORY_LIMIT` | "memory limit exceeded" | Query used too much memory | Use more specific filters or shorter time range |
|
||||
| `OVERCOMMIT` | "OvercommitTracker" | Memory overcommit limit hit | Reduce query complexity or result set size |
|
||||
| `TIMEOUT` | "Timeout", "timed out" | Query took too long | Add filters, reduce time range, or optimize query |
|
||||
|
||||
**ClickHouse retries:**
|
||||
|
||||
ClickHouse queries automatically retry network errors (socket hang up) with exponential backoff. Configure retry behavior:
|
||||
|
||||
```typescript
|
||||
// In packages/shared/src/env.ts
|
||||
LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS: z.coerce.number().positive().default(3)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
|
||||
- [SKILL.md](../SKILL.md) - Main backend development guidelines
|
||||
- [architecture-overview.md](architecture-overview.md) - System architecture
|
||||
- [configuration.md](configuration.md) - Environment variable configuration
|
||||
@@ -1,765 +0,0 @@
|
||||
# Middleware Guide - tRPC & Public API Patterns
|
||||
|
||||
Complete guide to middleware patterns in Langfuse's Next.js + tRPC architecture.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [tRPC Middleware](#trpc-middleware)
|
||||
- [Public API Middleware](#public-api-middleware)
|
||||
- [Authentication Patterns](#authentication-patterns)
|
||||
- [Error Handling Middleware](#error-handling-middleware)
|
||||
- [OpenTelemetry Instrumentation](#opentelemetry-instrumentation)
|
||||
- [Composable Procedures](#composable-procedures)
|
||||
|
||||
---
|
||||
|
||||
## tRPC Middleware
|
||||
|
||||
**File:** `web/src/server/api/trpc.ts`
|
||||
|
||||
tRPC middleware in Langfuse is composable and type-safe. Each middleware enriches the context and provides guarantees to subsequent middleware.
|
||||
|
||||
### Core tRPC Middlewares
|
||||
|
||||
**1. Error Handling Middleware (`withErrorHandling`)**
|
||||
|
||||
Intercepts all errors and transforms them into user-friendly tRPC errors:
|
||||
|
||||
```typescript
|
||||
const withErrorHandling = t.middleware(async ({ ctx, next }) => {
|
||||
const res = await next({ ctx });
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.error.cause instanceof ClickHouseResourceError) {
|
||||
// Surface ClickHouse resource errors with advice message
|
||||
res.error = new TRPCError({
|
||||
code: "SERVICE_UNAVAILABLE",
|
||||
message: ClickHouseResourceError.ERROR_ADVICE_MESSAGE,
|
||||
});
|
||||
} else {
|
||||
// Transform 5xx errors to not expose internals
|
||||
const { code, httpStatus } = resolveError(res.error);
|
||||
const isSafeToExpose = httpStatus >= 400 && httpStatus < 500;
|
||||
|
||||
res.error = new TRPCError({
|
||||
code,
|
||||
cause: null, // do not expose stack traces
|
||||
message: isSafeToExpose
|
||||
? res.error.message
|
||||
: "Internal error. We have been notified and are working on it.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
```
|
||||
|
||||
**2. OpenTelemetry Instrumentation (`withOtelInstrumentation`)**
|
||||
|
||||
Propagates OpenTelemetry context with Langfuse-specific baggage:
|
||||
|
||||
```typescript
|
||||
const withOtelInstrumentation = t.middleware(async (opts) => {
|
||||
const actualInput = await opts.getRawInput();
|
||||
|
||||
const baggageCtx = contextWithLangfuseProps({
|
||||
headers: opts.ctx.headers,
|
||||
userId: opts.ctx.session?.user?.id,
|
||||
projectId: (actualInput as Record<string, string>)?.projectId,
|
||||
});
|
||||
|
||||
return opentelemetry.context.with(baggageCtx, () => opts.next());
|
||||
});
|
||||
```
|
||||
|
||||
**3. Authentication Middleware (`enforceUserIsAuthed`)**
|
||||
|
||||
Ensures user is logged in via NextAuth session:
|
||||
|
||||
```typescript
|
||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.session.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**4. Project Membership Middleware (`enforceUserIsAuthedAndProjectMember`)**
|
||||
|
||||
Validates that the user is a member of the project specified in input:
|
||||
|
||||
```typescript
|
||||
const enforceUserIsAuthedAndProjectMember = t.middleware(async (opts) => {
|
||||
const { ctx, next } = opts;
|
||||
|
||||
if (!ctx.session || !ctx.session.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const actualInput = await opts.getRawInput();
|
||||
const parsedInput = inputProjectSchema.safeParse(actualInput);
|
||||
|
||||
if (!parsedInput.success) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid input, projectId is required",
|
||||
});
|
||||
}
|
||||
|
||||
const projectId = parsedInput.data.projectId;
|
||||
const sessionProject = ctx.session.user.organizations
|
||||
.flatMap((org) => org.projects.map((project) => ({ ...project, organization: org })))
|
||||
.find((project) => project.id === projectId);
|
||||
|
||||
if (!sessionProject) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "User is not a member of this project",
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
session: {
|
||||
...ctx.session,
|
||||
user: ctx.session.user,
|
||||
orgId: sessionProject.organization.id,
|
||||
orgRole: sessionProject.organization.role,
|
||||
projectId: projectId,
|
||||
projectRole: sessionProject.role,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**5. Organization Membership Middleware (`enforceIsAuthedAndOrgMember`)**
|
||||
|
||||
Validates organization membership:
|
||||
|
||||
```typescript
|
||||
const enforceIsAuthedAndOrgMember = t.middleware(async (opts) => {
|
||||
const { ctx, next } = opts;
|
||||
|
||||
if (!ctx.session || !ctx.session.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const actualInput = await opts.getRawInput();
|
||||
const result = inputOrganizationSchema.safeParse(actualInput);
|
||||
|
||||
if (!result.success) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid input, orgId is required",
|
||||
});
|
||||
}
|
||||
|
||||
const orgId = result.data.orgId;
|
||||
const sessionOrg = ctx.session.user.organizations.find((org) => org.id === orgId);
|
||||
|
||||
if (!sessionOrg && ctx.session.user.admin !== true) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "User is not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
session: {
|
||||
...ctx.session,
|
||||
user: ctx.session.user,
|
||||
orgId: orgId,
|
||||
orgRole: ctx.session.user.admin === true ? Role.OWNER : sessionOrg!.role,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**6. Trace Access Middleware (`enforceTraceAccess`)**
|
||||
|
||||
Special middleware for trace-level routes that supports public traces:
|
||||
|
||||
```typescript
|
||||
const enforceTraceAccess = t.middleware(async (opts) => {
|
||||
const { ctx, next } = opts;
|
||||
const actualInput = await opts.getRawInput();
|
||||
const result = inputTraceSchema.safeParse(actualInput);
|
||||
|
||||
if (!result.success) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid input" });
|
||||
}
|
||||
|
||||
const trace = await getTraceById({
|
||||
traceId: result.data.traceId,
|
||||
projectId: result.data.projectId,
|
||||
timestamp: result.data.timestamp ?? undefined,
|
||||
});
|
||||
|
||||
if (!trace) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Trace not found" });
|
||||
}
|
||||
|
||||
const sessionProject = ctx.session?.user?.organizations
|
||||
.flatMap((org) => org.projects)
|
||||
.find(({ id }) => id === result.data.projectId);
|
||||
|
||||
// Allow access if:
|
||||
// 1. User is a project member
|
||||
// 2. Trace is public
|
||||
// 3. User is admin
|
||||
if (!trace.public && !sessionProject && ctx.session?.user?.admin !== true) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "User is not a member of this project and this trace is not public",
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
session: { ...ctx.session, projectRole: sessionProject?.role },
|
||||
trace: trace, // pass the trace to avoid refetching
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### tRPC Procedure Types
|
||||
|
||||
Langfuse exports composed procedures with middleware chains:
|
||||
|
||||
```typescript
|
||||
// 1. Public procedure (no auth required)
|
||||
export const publicProcedure = withOtelTracingProcedure.use(withErrorHandling);
|
||||
|
||||
// 2. Authenticated procedure (NextAuth session required)
|
||||
export const authenticatedProcedure = withOtelTracingProcedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceUserIsAuthed);
|
||||
|
||||
// 3. Project-scoped procedure (project membership required)
|
||||
export const protectedProjectProcedure = withOtelTracingProcedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceUserIsAuthedAndProjectMember);
|
||||
|
||||
// 4. Organization-scoped procedure
|
||||
export const protectedOrganizationProcedure = withOtelTracingProcedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceIsAuthedAndOrgMember);
|
||||
|
||||
// 5. Trace access procedure (public traces supported)
|
||||
export const protectedGetTraceProcedure = withOtelTracingProcedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceTraceAccess);
|
||||
|
||||
// 6. Session access procedure
|
||||
export const protectedGetSessionProcedure = withOtelTracingProcedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceSessionAccess);
|
||||
|
||||
// 7. Admin API key procedure (for admin operations)
|
||||
export const adminProcedure = withOtelTracingProcedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceAdminAuth);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Public API Middleware
|
||||
|
||||
**Files:** `web/src/features/public-api/server/`
|
||||
|
||||
### withMiddlewares Pattern
|
||||
|
||||
Wraps all public API routes with CORS, error handling, and OpenTelemetry:
|
||||
|
||||
```typescript
|
||||
export function withMiddlewares(handlers: Handlers) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const ctx = contextWithLangfuseProps({ headers: req.headers });
|
||||
|
||||
return opentelemetry.context.with(ctx, async () => {
|
||||
try {
|
||||
// 1. CORS middleware
|
||||
await runMiddleware(req, res, cors);
|
||||
|
||||
// 2. HTTP method routing
|
||||
const method = req.method as HttpMethod;
|
||||
if (!handlers[method]) throw new MethodNotAllowedError();
|
||||
|
||||
// 3. Execute handler
|
||||
return await handlers[method](req, res);
|
||||
} catch (error) {
|
||||
// 4. Error handling
|
||||
if (error instanceof BaseError) {
|
||||
if (error.httpCode >= 500) traceException(error);
|
||||
|
||||
return res.status(error.httpCode).json({
|
||||
message: error.message,
|
||||
error: error.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof ClickHouseResourceError) {
|
||||
return res.status(524).json({
|
||||
message: ClickHouseResourceError.ERROR_ADVICE_MESSAGE,
|
||||
error: "Request is taking too long to process.",
|
||||
});
|
||||
}
|
||||
|
||||
if (isZodError(error)) {
|
||||
return res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
traceException(error);
|
||||
return res.status(500).json({
|
||||
message: "Internal Server Error",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// web/src/pages/api/public/datasets/[datasetName]/items.ts
|
||||
export default withMiddlewares({
|
||||
GET: getDatasetItemsHandler,
|
||||
POST: createDatasetItemHandler,
|
||||
});
|
||||
```
|
||||
|
||||
### createAuthedProjectAPIRoute Pattern
|
||||
|
||||
Factory function for authenticated public API routes with:
|
||||
- Authentication (Basic auth or Admin API key)
|
||||
- Rate limiting
|
||||
- Input/output validation (Zod)
|
||||
- OpenTelemetry context
|
||||
|
||||
```typescript
|
||||
export const createAuthedProjectAPIRoute = <TQuery, TBody, TResponse>(
|
||||
routeConfig: RouteConfig<TQuery, TBody, TResponse>
|
||||
) => {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// 1. Authentication (verifyAuth)
|
||||
const auth = await verifyAuth(req, routeConfig.isAdminApiKeyAuthAllowed || false);
|
||||
|
||||
// 2. Rate limiting
|
||||
const rateLimitResponse = await RateLimitService.getInstance().rateLimitRequest(
|
||||
auth.scope,
|
||||
routeConfig.rateLimitResource || "public-api"
|
||||
);
|
||||
|
||||
if (rateLimitResponse?.isRateLimited()) {
|
||||
return rateLimitResponse.sendRestResponseIfLimited(res);
|
||||
}
|
||||
|
||||
// 3. Input validation
|
||||
const query = routeConfig.querySchema
|
||||
? routeConfig.querySchema.parse(req.query)
|
||||
: {};
|
||||
const body = routeConfig.bodySchema
|
||||
? routeConfig.bodySchema.parse(req.body)
|
||||
: {};
|
||||
|
||||
// 4. Execute with OpenTelemetry context
|
||||
const ctx = contextWithLangfuseProps({
|
||||
headers: req.headers,
|
||||
projectId: auth.scope.projectId,
|
||||
});
|
||||
|
||||
return opentelemetry.context.with(ctx, async () => {
|
||||
const response = await routeConfig.fn({ query, body, req, res, auth });
|
||||
|
||||
// 5. Response validation (dev only)
|
||||
if (env.NODE_ENV === "development" && routeConfig.responseSchema) {
|
||||
const parsingResult = routeConfig.responseSchema.safeParse(response);
|
||||
if (!parsingResult.success) {
|
||||
logger.error("Response validation failed:", parsingResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(routeConfig.successStatusCode || 200).json(response);
|
||||
});
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// web/src/pages/api/public/traces/[traceId].ts
|
||||
export default createAuthedProjectAPIRoute({
|
||||
name: "Get Trace",
|
||||
querySchema: GetTraceV1Query,
|
||||
responseSchema: GetTraceV1Response,
|
||||
fn: async ({ query, auth }) => {
|
||||
const trace = await getTraceById({
|
||||
traceId: query.traceId,
|
||||
projectId: auth.scope.projectId,
|
||||
});
|
||||
|
||||
return transformTraceToApiResponse(trace);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Patterns
|
||||
|
||||
### tRPC Authentication (NextAuth)
|
||||
|
||||
tRPC uses NextAuth sessions stored in JWT cookies:
|
||||
|
||||
```typescript
|
||||
// Context creation with session
|
||||
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||
const { req, res } = opts;
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
addUserToSpan({
|
||||
userId: session?.user?.id,
|
||||
email: session?.user?.email ?? undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
session,
|
||||
headers: req.headers,
|
||||
prisma,
|
||||
DB,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Session types:**
|
||||
|
||||
```typescript
|
||||
// Base authenticated context
|
||||
export type AuthedContext = {
|
||||
session: { user: NonNullable<Session["user"]> };
|
||||
};
|
||||
|
||||
// Project-scoped context
|
||||
export type ProjectAuthedContext = {
|
||||
session: AuthedContext["session"] & {
|
||||
orgId: string;
|
||||
orgRole: Role;
|
||||
projectId: string;
|
||||
projectRole: Role;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Public API Authentication
|
||||
|
||||
Public APIs use **Basic Auth** with API keys:
|
||||
|
||||
```typescript
|
||||
async function verifyBasicAuth(authHeader: string | undefined) {
|
||||
const regularAuth = await new ApiAuthService(prisma, redis)
|
||||
.verifyAuthHeaderAndReturnScope(authHeader);
|
||||
|
||||
if (!regularAuth.validKey) {
|
||||
throw { status: 401, message: regularAuth.error };
|
||||
}
|
||||
|
||||
if (regularAuth.scope.accessLevel !== "project") {
|
||||
throw { status: 401, message: "Access denied - need basic auth with secret key" };
|
||||
}
|
||||
|
||||
return regularAuth;
|
||||
}
|
||||
```
|
||||
|
||||
**Admin API Key Authentication** (self-hosted only):
|
||||
|
||||
```typescript
|
||||
async function verifyAdminApiKeyAuth(req: NextApiRequest) {
|
||||
// Requires:
|
||||
// 1. Authorization: Bearer <ADMIN_API_KEY>
|
||||
// 2. x-langfuse-admin-api-key: <ADMIN_API_KEY>
|
||||
// 3. x-langfuse-project-id: <project-id>
|
||||
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
|
||||
throw { status: 403, message: "Admin API key auth not available on Langfuse Cloud" };
|
||||
}
|
||||
|
||||
const adminApiKey = env.ADMIN_API_KEY;
|
||||
const bearerToken = req.headers.authorization?.replace("Bearer ", "");
|
||||
const adminApiKeyHeader = req.headers["x-langfuse-admin-api-key"];
|
||||
|
||||
// Timing-safe comparison
|
||||
const isValid =
|
||||
crypto.timingSafeEqual(Buffer.from(bearerToken), Buffer.from(adminApiKey)) &&
|
||||
crypto.timingSafeEqual(Buffer.from(adminApiKeyHeader), Buffer.from(adminApiKey));
|
||||
|
||||
if (!isValid) throw { status: 401, message: "Invalid admin API key" };
|
||||
|
||||
const projectId = req.headers["x-langfuse-project-id"];
|
||||
const project = await prisma.project.findUnique({ where: { id: projectId } });
|
||||
|
||||
if (!project) throw { status: 404, message: "Project not found" };
|
||||
|
||||
return { validKey: true, scope: { projectId, accessLevel: "project" } };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Middleware
|
||||
|
||||
### tRPC Error Transformation
|
||||
|
||||
All tRPC errors go through `withErrorHandling` middleware:
|
||||
|
||||
**Error types handled:**
|
||||
|
||||
1. **ClickHouseResourceError** → `SERVICE_UNAVAILABLE` (524)
|
||||
2. **BaseError** → Preserves httpCode and message
|
||||
3. **5xx errors** → Sanitized as "Internal error" (hides stack traces)
|
||||
4. **4xx errors** → Original error message preserved
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
if (!res.ok) {
|
||||
if (res.error.cause instanceof ClickHouseResourceError) {
|
||||
res.error = new TRPCError({
|
||||
code: "SERVICE_UNAVAILABLE",
|
||||
message: ClickHouseResourceError.ERROR_ADVICE_MESSAGE,
|
||||
});
|
||||
} else {
|
||||
const { code, httpStatus } = resolveError(res.error);
|
||||
const isSafeToExpose = httpStatus >= 400 && httpStatus < 500;
|
||||
|
||||
res.error = new TRPCError({
|
||||
code,
|
||||
cause: null,
|
||||
message: isSafeToExpose ? res.error.message : "Internal error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Public API Error Handling
|
||||
|
||||
Public API uses `withMiddlewares` for error handling:
|
||||
|
||||
```typescript
|
||||
catch (error) {
|
||||
// 1. BaseError (custom application errors)
|
||||
if (error instanceof BaseError) {
|
||||
if (error.httpCode >= 500) traceException(error);
|
||||
return res.status(error.httpCode).json({
|
||||
message: error.message,
|
||||
error: error.name,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. ClickHouseResourceError (query timeouts, memory limits)
|
||||
if (error instanceof ClickHouseResourceError) {
|
||||
return res.status(524).json({
|
||||
message: ClickHouseResourceError.ERROR_ADVICE_MESSAGE,
|
||||
error: "Request is taking too long to process.",
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Zod validation errors
|
||||
if (isZodError(error)) {
|
||||
return res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Prisma errors
|
||||
if (isPrismaException(error)) {
|
||||
traceException(error);
|
||||
return res.status(500).json({
|
||||
message: "Internal Server Error",
|
||||
error: "An unknown error occurred",
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Unknown errors
|
||||
traceException(error);
|
||||
return res.status(500).json({
|
||||
message: "Internal Server Error",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OpenTelemetry Instrumentation
|
||||
|
||||
All requests (tRPC and public API) propagate OpenTelemetry context with Langfuse-specific baggage.
|
||||
|
||||
### Context Propagation Pattern
|
||||
|
||||
```typescript
|
||||
import { contextWithLangfuseProps } from "@langfuse/shared/src/server";
|
||||
import * as opentelemetry from "@opentelemetry/api";
|
||||
|
||||
// Create context with Langfuse baggage
|
||||
const ctx = contextWithLangfuseProps({
|
||||
headers: req.headers,
|
||||
userId: session?.user?.id,
|
||||
projectId: input?.projectId,
|
||||
});
|
||||
|
||||
// Execute with context
|
||||
return opentelemetry.context.with(ctx, async () => {
|
||||
// All instrumented code inside here will have access to baggage
|
||||
return await handler();
|
||||
});
|
||||
```
|
||||
|
||||
**Baggage includes:**
|
||||
- `userId` - User ID from session
|
||||
- `projectId` - Project ID from input
|
||||
- `headers` - Request headers for trace propagation
|
||||
|
||||
### tRPC Instrumentation
|
||||
|
||||
```typescript
|
||||
const withOtelInstrumentation = t.middleware(async (opts) => {
|
||||
const actualInput = await opts.getRawInput();
|
||||
|
||||
const baggageCtx = contextWithLangfuseProps({
|
||||
headers: opts.ctx.headers,
|
||||
userId: opts.ctx.session?.user?.id,
|
||||
projectId: (actualInput as Record<string, string>)?.projectId,
|
||||
});
|
||||
|
||||
return opentelemetry.context.with(baggageCtx, () => opts.next());
|
||||
});
|
||||
|
||||
// Used with Baselime tracing
|
||||
const withOtelTracingProcedure = t.procedure
|
||||
.use(withOtelInstrumentation)
|
||||
.use(tracing({ collectInput: true, collectResult: true }));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composable Procedures
|
||||
|
||||
tRPC procedures are composed by chaining middleware:
|
||||
|
||||
### Composition Pattern
|
||||
|
||||
```typescript
|
||||
// Base procedure with tracing + error handling
|
||||
const baseProcedure = withOtelTracingProcedure.use(withErrorHandling);
|
||||
|
||||
// Add authentication
|
||||
const authedProcedure = baseProcedure.use(enforceUserIsAuthed);
|
||||
|
||||
// Add project scoping
|
||||
const projectProcedure = authedProcedure.use(enforceUserIsAuthedAndProjectMember);
|
||||
```
|
||||
|
||||
### Using Procedures in Routers
|
||||
|
||||
```typescript
|
||||
import { protectedProjectProcedure } from "@/src/server/api/trpc";
|
||||
|
||||
export const tracesRouter = createTRPCRouter({
|
||||
// Input automatically validated against Zod schema
|
||||
all: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
page: z.number().optional(),
|
||||
limit: z.number().optional(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// ctx.session.projectId is guaranteed to exist
|
||||
// ctx.session.projectRole contains user's role
|
||||
|
||||
const traces = await getTraces({
|
||||
projectId: input.projectId,
|
||||
page: input.page ?? 0,
|
||||
limit: input.limit ?? 50,
|
||||
});
|
||||
|
||||
return traces;
|
||||
}),
|
||||
|
||||
byId: protectedGetTraceProcedure
|
||||
.input(
|
||||
z.object({
|
||||
traceId: z.string(),
|
||||
projectId: z.string(),
|
||||
timestamp: z.date().nullish(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// ctx.trace is guaranteed to exist (fetched by middleware)
|
||||
// No need to refetch
|
||||
return ctx.trace;
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Middleware Execution Order
|
||||
|
||||
Middleware executes in the order it's chained:
|
||||
|
||||
```typescript
|
||||
protectedProjectProcedure
|
||||
.use(withErrorHandling) // 1. Wraps entire execution
|
||||
.use(enforceUserIsAuthed) // 2. Validates session exists
|
||||
.use(enforceUserIsAuthedAndProjectMember) // 3. Validates project membership
|
||||
.input(schema) // 4. Validates input
|
||||
.query(async ({ input, ctx }) => { // 5. Executes query
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Context enrichment:**
|
||||
|
||||
Each middleware can enrich the context:
|
||||
|
||||
```typescript
|
||||
// After enforceUserIsAuthed:
|
||||
ctx.session.user // NonNullable<User>
|
||||
|
||||
// After enforceUserIsAuthedAndProjectMember:
|
||||
ctx.session.projectId // string
|
||||
ctx.session.projectRole // Role (OWNER | ADMIN | MEMBER | VIEWER)
|
||||
ctx.session.orgId // string
|
||||
ctx.session.orgRole // Role
|
||||
|
||||
// After enforceTraceAccess:
|
||||
ctx.trace // TraceRecord (pre-fetched)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
|
||||
- [SKILL.md](../SKILL.md) - Main backend development guidelines
|
||||
- [architecture-overview.md](architecture-overview.md) - System architecture
|
||||
- [async-and-errors.md](async-and-errors.md) - Error handling patterns
|
||||
@@ -1,844 +0,0 @@
|
||||
# Routing Patterns - Next.js & tRPC
|
||||
|
||||
Complete guide to routing and separation of concerns in Langfuse's Next.js + tRPC architecture.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [tRPC Routers](#trpc-routers)
|
||||
- [Public REST API Routes](#public-rest-api-routes)
|
||||
- [Service Layer](#service-layer)
|
||||
- [Repository Layer](#repository-layer)
|
||||
- [Separation of Concerns](#separation-of-concerns)
|
||||
- [Anti-Patterns](#anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Langfuse uses a **layered architecture** with clear separation of concerns:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ENTRY POINTS │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ tRPC Procedures │ │ Public REST API Routes │ │
|
||||
│ │ (Internal UI API) │ │ (SDK/External API) │ │
|
||||
│ └──────────────────────┘ └─────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SERVICE LAYER │
|
||||
│ Business logic, orchestration, validation │
|
||||
│ web/src/features/*/server/ or packages/shared/services/ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ REPOSITORY LAYER │
|
||||
│ Complex queries, data transformation │
|
||||
│ packages/shared/src/server/repositories/ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DATABASE LAYER │
|
||||
│ PostgreSQL (Prisma) + ClickHouse (Direct Client) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Principles
|
||||
|
||||
**Entry Points (Routes/Procedures):**
|
||||
- ✅ Define routing and procedure signatures
|
||||
- ✅ Handle authentication/authorization (via middleware)
|
||||
- ✅ Validate input (Zod schemas)
|
||||
- ✅ Delegate to services
|
||||
- ✅ Return responses
|
||||
|
||||
**Entry Points should NEVER:**
|
||||
- ❌ Contain business logic
|
||||
- ❌ Access database directly
|
||||
- ❌ Perform complex data transformations
|
||||
- ❌ Make direct repository calls (use services)
|
||||
|
||||
**Services:**
|
||||
- ✅ Contain business logic
|
||||
- ✅ Orchestrate multiple operations
|
||||
- ✅ Call repositories or Prisma/ClickHouse
|
||||
- ✅ Handle complex workflows
|
||||
- ❌ Should NOT know about HTTP, tRPC, or request/response objects
|
||||
|
||||
**Repositories:**
|
||||
- ✅ Complex database queries
|
||||
- ✅ Data transformation (DB → domain models)
|
||||
- ✅ ClickHouse query builders
|
||||
- ✅ Reusable query logic
|
||||
- ❌ Should NOT contain business logic
|
||||
|
||||
---
|
||||
|
||||
## tRPC Routers
|
||||
|
||||
**Location:** `web/src/server/api/routers/`
|
||||
|
||||
tRPC routers define type-safe procedures for the internal UI. Each router groups related operations.
|
||||
|
||||
### Router Structure
|
||||
|
||||
**File:** `web/src/server/api/routers/scores.ts`
|
||||
|
||||
```typescript
|
||||
import { z } from "zod/v4";
|
||||
import { createTRPCRouter, protectedProjectProcedure } from "@/src/server/api/trpc";
|
||||
import { paginationZod, singleFilter, orderBy } from "@langfuse/shared";
|
||||
import {
|
||||
getScoresUiTable,
|
||||
getScoresUiCount,
|
||||
upsertScore,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
const ScoreAllOptions = z.object({
|
||||
projectId: z.string(),
|
||||
filter: z.array(singleFilter),
|
||||
orderBy: orderBy,
|
||||
...paginationZod,
|
||||
});
|
||||
|
||||
export const scoresRouter = createTRPCRouter({
|
||||
/**
|
||||
* Get all scores for a project
|
||||
*/
|
||||
all: protectedProjectProcedure
|
||||
.input(ScoreAllOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// Delegate to repository for data fetching
|
||||
const clickhouseScoreData = await getScoresUiTable({
|
||||
projectId: input.projectId,
|
||||
filter: input.filter ?? [],
|
||||
orderBy: input.orderBy,
|
||||
limit: input.limit,
|
||||
offset: input.page * input.limit,
|
||||
});
|
||||
|
||||
// Delegate to Prisma for related data
|
||||
const [jobExecutions, users] = await Promise.all([
|
||||
ctx.prisma.jobExecution.findMany({
|
||||
where: {
|
||||
jobOutputScoreId: {
|
||||
in: clickhouseScoreData.map((score) => score.id),
|
||||
},
|
||||
},
|
||||
}),
|
||||
ctx.prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: clickhouseScoreData
|
||||
.map((s) => s.authorUserId)
|
||||
.filter((id): id is string => id !== null),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Transform and combine data
|
||||
return clickhouseScoreData.map((score) => ({
|
||||
...score,
|
||||
jobConfigurationId:
|
||||
jobExecutions.find((j) => j.jobOutputScoreId === score.id)
|
||||
?.jobConfigurationId ?? null,
|
||||
authorUserImage:
|
||||
users.find((u) => u.id === score.authorUserId)?.image ?? null,
|
||||
authorUserName:
|
||||
users.find((u) => u.id === score.authorUserId)?.name ?? null,
|
||||
}));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create or update score
|
||||
*/
|
||||
createAnnotationScore: protectedProjectProcedure
|
||||
.input(CreateAnnotationScoreData)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Validation
|
||||
validateConfigAgainstBody(input);
|
||||
|
||||
// Delegate to repository
|
||||
await upsertScore({
|
||||
id: input.id ?? randomUUID(),
|
||||
traceId: input.traceId,
|
||||
projectId: input.projectId,
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
source: ScoreSource.ANNOTATION,
|
||||
authorUserId: ctx.session.user.id,
|
||||
comment: input.comment,
|
||||
});
|
||||
|
||||
// Audit log
|
||||
await auditLog({
|
||||
session: ctx.session,
|
||||
resourceType: "score",
|
||||
resourceId: input.id,
|
||||
action: "create",
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use appropriate procedure type (`protectedProjectProcedure`, `authenticatedProcedure`, etc.)
|
||||
- Define input schema with Zod (`.input()`)
|
||||
- Use `.query()` for reads, `.mutation()` for writes
|
||||
- Delegate to services/repositories for data access
|
||||
- Keep procedures thin - no business logic
|
||||
- Type-safe throughout (TypeScript infers types from Zod schemas)
|
||||
|
||||
### Registering Routers
|
||||
|
||||
**File:** `web/src/server/api/root.ts`
|
||||
|
||||
```typescript
|
||||
import { createTRPCRouter } from "@/src/server/api/trpc";
|
||||
import { scoresRouter } from "./routers/scores";
|
||||
import { tracesRouter } from "./routers/traces";
|
||||
import { dashboardRouter } from "@/src/features/dashboard/server/dashboard-router";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
scores: scoresRouter,
|
||||
traces: tracesRouter,
|
||||
dashboard: dashboardRouter,
|
||||
// ... other routers
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
```
|
||||
|
||||
**Calling from frontend:**
|
||||
|
||||
```typescript
|
||||
// Type-safe client call
|
||||
const { data, isLoading } = api.scores.all.useQuery({
|
||||
projectId: "proj_123",
|
||||
page: 0,
|
||||
limit: 50,
|
||||
filter: [],
|
||||
orderBy: null,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Public REST API Routes
|
||||
|
||||
**Location:** `web/src/pages/api/public/`
|
||||
|
||||
Public API routes use **Next.js file-based routing** and provide REST endpoints for SDKs and external integrations.
|
||||
|
||||
### File-based Routing
|
||||
|
||||
Next.js uses file system for routing:
|
||||
|
||||
```
|
||||
web/src/pages/api/public/
|
||||
├── scores/
|
||||
│ ├── index.ts → GET/POST /api/public/scores
|
||||
│ └── [scoreId].ts → GET/PATCH/DELETE /api/public/scores/:scoreId
|
||||
├── traces/
|
||||
│ ├── index.ts → GET /api/public/traces
|
||||
│ └── [traceId].ts → GET /api/public/traces/:traceId
|
||||
└── datasets/
|
||||
└── [name]/
|
||||
├── index.ts → GET/POST /api/public/datasets/:name
|
||||
└── items/
|
||||
└── index.ts → GET /api/public/datasets/:name/items
|
||||
```
|
||||
|
||||
**Dynamic routes:**
|
||||
- `[param].ts` → Single dynamic segment (e.g., `/api/public/scores/[scoreId].ts`)
|
||||
- `[...param].ts` → Catch-all route (e.g., `/api/public/[...path].ts`)
|
||||
|
||||
### REST API Pattern
|
||||
|
||||
**File:** `web/src/pages/api/public/scores/index.ts`
|
||||
|
||||
```typescript
|
||||
import { v4 } from "uuid";
|
||||
import { createAuthedProjectAPIRoute } from "@/src/features/public-api/server/createAuthedProjectAPIRoute";
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import {
|
||||
GetScoresQueryV1,
|
||||
GetScoresResponseV1,
|
||||
PostScoresBodyV1,
|
||||
PostScoresResponseV1,
|
||||
} from "@langfuse/shared";
|
||||
import { eventTypes, processEventBatch } from "@langfuse/shared/src/server";
|
||||
import { ScoresApiService } from "@/src/features/public-api/server/scores-api-service";
|
||||
|
||||
export default withMiddlewares({
|
||||
// POST /api/public/scores
|
||||
POST: createAuthedProjectAPIRoute({
|
||||
name: "Create Score",
|
||||
bodySchema: PostScoresBodyV1,
|
||||
responseSchema: PostScoresResponseV1,
|
||||
fn: async ({ body, auth, res }) => {
|
||||
const event = {
|
||||
id: v4(),
|
||||
type: eventTypes.SCORE_CREATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body,
|
||||
};
|
||||
|
||||
if (!event.body.id) {
|
||||
event.body.id = v4();
|
||||
}
|
||||
|
||||
const result = await processEventBatch([event], auth);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
const error = result.errors[0];
|
||||
res.status(error.status).json({
|
||||
message: error.error ?? error.message,
|
||||
});
|
||||
return { id: "" };
|
||||
}
|
||||
|
||||
return { id: event.body.id };
|
||||
},
|
||||
}),
|
||||
|
||||
// GET /api/public/scores
|
||||
GET: createAuthedProjectAPIRoute({
|
||||
name: "Get Scores",
|
||||
querySchema: GetScoresQueryV1,
|
||||
responseSchema: GetScoresResponseV1,
|
||||
fn: async ({ query, auth }) => {
|
||||
const scoresApiService = new ScoresApiService("v1");
|
||||
|
||||
const [items, count] = await Promise.all([
|
||||
scoresApiService.generateScoresForPublicApi({
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
userId: query.userId,
|
||||
name: query.name,
|
||||
}),
|
||||
scoresApiService.getScoresCountForPublicApi({
|
||||
projectId: auth.scope.projectId,
|
||||
userId: query.userId,
|
||||
name: query.name,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: items,
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems: count,
|
||||
totalPages: Math.ceil(count / query.limit),
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use `withMiddlewares` for all public API routes (provides CORS, error handling, OpenTelemetry)
|
||||
- Use `createAuthedProjectAPIRoute` for authenticated endpoints (handles auth, rate limiting, validation)
|
||||
- Define separate handlers for each HTTP method
|
||||
- Input/output validated with Zod schemas
|
||||
- Delegate to services for business logic
|
||||
|
||||
### Simple Public Routes
|
||||
|
||||
For routes that don't need authentication:
|
||||
|
||||
```typescript
|
||||
// web/src/pages/api/public/health.ts
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
|
||||
export default withMiddlewares({
|
||||
GET: async (req, res) => {
|
||||
res.status(200).json({ status: "ok" });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Layer
|
||||
|
||||
**Location:** `web/src/features/*/server/` or `packages/shared/src/server/services/`
|
||||
|
||||
Services contain business logic and orchestrate operations. They're called by tRPC procedures and API routes.
|
||||
|
||||
### Service Pattern
|
||||
|
||||
**File:** `web/src/features/public-api/server/scores-api-service.ts`
|
||||
|
||||
```typescript
|
||||
import {
|
||||
_handleGenerateScoresForPublicApi,
|
||||
_handleGetScoresCountForPublicApi,
|
||||
type ScoreQueryType,
|
||||
} from "@/src/features/public-api/server/scores";
|
||||
import { _handleGetScoreById } from "@langfuse/shared/src/server";
|
||||
|
||||
export class ScoresApiService {
|
||||
constructor(private readonly apiVersion: "v1" | "v2") {}
|
||||
|
||||
/**
|
||||
* Get a specific score by ID
|
||||
*/
|
||||
async getScoreById({
|
||||
projectId,
|
||||
scoreId,
|
||||
source,
|
||||
}: {
|
||||
projectId: string;
|
||||
scoreId: string;
|
||||
source?: ScoreSourceType;
|
||||
}) {
|
||||
return _handleGetScoreById({
|
||||
projectId,
|
||||
scoreId,
|
||||
source,
|
||||
scoreScope: this.apiVersion === "v1" ? "traces_only" : "all",
|
||||
preferredClickhouseService: "ReadOnly",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of scores with version-aware filtering
|
||||
*/
|
||||
async generateScoresForPublicApi(props: ScoreQueryType) {
|
||||
return _handleGenerateScoresForPublicApi({
|
||||
props,
|
||||
scoreScope: this.apiVersion === "v1" ? "traces_only" : "all",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of scores with version-aware filtering
|
||||
*/
|
||||
async getScoresCountForPublicApi(props: ScoreQueryType) {
|
||||
return _handleGetScoresCountForPublicApi({
|
||||
props,
|
||||
scoreScope: this.apiVersion === "v1" ? "traces_only" : "all",
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Services contain business logic, not routing logic
|
||||
- Services should NOT import tRPC or Next.js types
|
||||
- Services can call repositories, Prisma, ClickHouse directly
|
||||
- Services orchestrate multiple operations
|
||||
- Services are reusable across tRPC and public API
|
||||
|
||||
### Where to Put Services
|
||||
|
||||
**Feature-specific services:**
|
||||
```
|
||||
web/src/features/
|
||||
├── datasets/
|
||||
│ └── server/
|
||||
│ └── dataset-service.ts
|
||||
├── evals/
|
||||
│ └── server/
|
||||
│ └── eval-service.ts
|
||||
└── public-api/
|
||||
└── server/
|
||||
└── scores-api-service.ts
|
||||
```
|
||||
|
||||
**Shared services:**
|
||||
```
|
||||
packages/shared/src/server/services/
|
||||
├── SlackService.ts
|
||||
├── DashboardService/
|
||||
├── StorageService.ts
|
||||
└── DefaultEvaluationModelService/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Repository Layer
|
||||
|
||||
**Location:** `packages/shared/src/server/repositories/`
|
||||
|
||||
Repositories handle complex database queries, data transformation, and provide reusable query logic.
|
||||
|
||||
### Repository Structure
|
||||
|
||||
```
|
||||
packages/shared/src/server/repositories/
|
||||
├── traces.ts # Trace queries (ClickHouse)
|
||||
├── observations.ts # Observation queries (ClickHouse)
|
||||
├── scores.ts # Score queries (ClickHouse)
|
||||
├── clickhouse.ts # Core ClickHouse helpers
|
||||
└── definitions.ts # Type definitions
|
||||
```
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
**File:** `packages/shared/src/server/repositories/traces.ts`
|
||||
|
||||
```typescript
|
||||
import { queryClickhouse, upsertClickhouse } from "./clickhouse";
|
||||
import { TraceRecordReadType } from "./definitions";
|
||||
import { convertClickhouseToDomain } from "./traces_converters";
|
||||
|
||||
/**
|
||||
* Get traces by IDs
|
||||
*/
|
||||
export const getTracesByIds = async (
|
||||
projectId: string,
|
||||
traceIds: string[]
|
||||
): Promise<TraceRecordReadType[]> => {
|
||||
const rows = await queryClickhouse<TraceRecordReadType>({
|
||||
query: `
|
||||
SELECT *
|
||||
FROM traces
|
||||
WHERE project_id = {projectId: String}
|
||||
AND id IN ({traceIds: Array(String)})
|
||||
ORDER BY event_ts DESC
|
||||
LIMIT 1 BY id, project_id
|
||||
`,
|
||||
params: { projectId, traceIds },
|
||||
tags: { feature: "tracing", type: "trace" },
|
||||
});
|
||||
|
||||
return rows.map(convertClickhouseToDomain);
|
||||
};
|
||||
|
||||
/**
|
||||
* Upsert trace to ClickHouse
|
||||
*/
|
||||
export const upsertTrace = async (
|
||||
trace: TraceRecordInsertType
|
||||
): Promise<void> => {
|
||||
await upsertClickhouse({
|
||||
table: "traces",
|
||||
records: [trace],
|
||||
eventBodyMapper: (body) => ({
|
||||
id: body.id,
|
||||
name: body.name,
|
||||
user_id: body.user_id,
|
||||
// ... map fields
|
||||
}),
|
||||
tags: { feature: "ingestion", type: "trace" },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use `queryClickhouse` for SELECT queries
|
||||
- Use `upsertClickhouse` for INSERT/UPDATE
|
||||
- Use `commandClickhouse` for DDL (ALTER TABLE, etc.)
|
||||
- Include data converters (`convertClickhouseToDomain`)
|
||||
- Add OpenTelemetry tags for observability
|
||||
- Repositories should NOT contain business logic
|
||||
|
||||
### When to Use Repositories
|
||||
|
||||
✅ **Use repositories for:**
|
||||
- Complex ClickHouse queries with CTEs, joins, aggregations
|
||||
- Queries used in multiple places (DRY principle)
|
||||
- Data transformation from DB types to domain models
|
||||
- Streaming large result sets
|
||||
|
||||
❌ **Use direct Prisma/ClickHouse for:**
|
||||
- Simple CRUD operations
|
||||
- One-off queries
|
||||
- Prototyping (can refactor to repository later)
|
||||
|
||||
---
|
||||
|
||||
## Separation of Concerns
|
||||
|
||||
### ✅ Good Example: Proper Layering
|
||||
|
||||
**tRPC Procedure (Entry Point):**
|
||||
|
||||
```typescript
|
||||
// web/src/server/api/routers/scores.ts
|
||||
export const scoresRouter = createTRPCRouter({
|
||||
all: protectedProjectProcedure
|
||||
.input(ScoreFilterOptions)
|
||||
.query(async ({ input }) => {
|
||||
// ✅ Thin procedure - delegates to repository
|
||||
return await getScoresUiTable({
|
||||
projectId: input.projectId,
|
||||
filter: input.filter,
|
||||
orderBy: input.orderBy,
|
||||
});
|
||||
}),
|
||||
|
||||
create: protectedProjectProcedure
|
||||
.input(CreateScoreInput)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// ✅ Delegates to service for orchestration
|
||||
return await createScoreWithValidation({
|
||||
scoreData: input,
|
||||
userId: ctx.session.user.id,
|
||||
projectId: ctx.session.projectId,
|
||||
});
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
**Service (Business Logic):**
|
||||
|
||||
```typescript
|
||||
// web/src/features/scores/server/score-service.ts
|
||||
export async function createScoreWithValidation({
|
||||
scoreData,
|
||||
userId,
|
||||
projectId,
|
||||
}: {
|
||||
scoreData: CreateScoreInput;
|
||||
userId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
// ✅ Business logic: validation
|
||||
const config = await prisma.scoreConfig.findUnique({
|
||||
where: { id: scoreData.configId },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
throw new LangfuseNotFoundError("Score config not found");
|
||||
}
|
||||
|
||||
validateConfigAgainstBody(config, scoreData);
|
||||
|
||||
// ✅ Business logic: orchestration
|
||||
const scoreId = randomUUID();
|
||||
|
||||
await Promise.all([
|
||||
// Create score in ClickHouse
|
||||
upsertScore({
|
||||
id: scoreId,
|
||||
projectId,
|
||||
traceId: scoreData.traceId,
|
||||
name: scoreData.name,
|
||||
value: scoreData.value,
|
||||
authorUserId: userId,
|
||||
}),
|
||||
// Audit log in PostgreSQL
|
||||
auditLog({
|
||||
userId,
|
||||
resourceType: "score",
|
||||
resourceId: scoreId,
|
||||
action: "create",
|
||||
}),
|
||||
]);
|
||||
|
||||
return { id: scoreId };
|
||||
}
|
||||
```
|
||||
|
||||
**Repository (Data Access):**
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/server/repositories/scores.ts
|
||||
export const upsertScore = async (
|
||||
score: ScoreInsertType
|
||||
): Promise<void> => {
|
||||
// ✅ Pure data access - no business logic
|
||||
await upsertClickhouse({
|
||||
table: "scores",
|
||||
records: [score],
|
||||
eventBodyMapper: (body) => ({
|
||||
id: body.id,
|
||||
trace_id: body.traceId,
|
||||
name: body.name,
|
||||
value: body.value,
|
||||
author_user_id: body.authorUserId,
|
||||
}),
|
||||
tags: { feature: "scoring" },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
1. **tRPC Procedure**: Thin, delegates to service
|
||||
2. **Service**: Contains all business logic (validation, orchestration)
|
||||
3. **Repository**: Pure data access, reusable
|
||||
4. **Service is protocol-agnostic**: Can be called from tRPC, public API, or worker
|
||||
5. **Clear separation**: Easy to test, maintain, extend
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### ❌ Anti-Pattern 1: Business Logic in Routes
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Business logic in tRPC procedure
|
||||
export const scoresRouter = createTRPCRouter({
|
||||
create: protectedProjectProcedure
|
||||
.input(CreateScoreInput)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// ❌ Validation logic in route
|
||||
const config = await ctx.prisma.scoreConfig.findUnique({
|
||||
where: { id: input.configId },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
if (config.dataType === "NUMERIC" && typeof input.value !== "number") {
|
||||
throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
}
|
||||
|
||||
// ❌ Direct database access
|
||||
await ctx.prisma.score.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
projectId: ctx.session.projectId,
|
||||
traceId: input.traceId,
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
},
|
||||
});
|
||||
|
||||
// ❌ More business logic
|
||||
await auditLog({ ... });
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
**Why it's bad:**
|
||||
- Business logic tied to tRPC (can't reuse in public API)
|
||||
- Hard to test (need to mock tRPC context)
|
||||
- No separation of concerns
|
||||
- Difficult to maintain
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: Thin procedure, delegates to service
|
||||
export const scoresRouter = createTRPCRouter({
|
||||
create: protectedProjectProcedure
|
||||
.input(CreateScoreInput)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return await createScoreWithValidation({
|
||||
scoreData: input,
|
||||
userId: ctx.session.user.id,
|
||||
projectId: ctx.session.projectId,
|
||||
});
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Anti-Pattern 2: Database Calls in Routes
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Direct database access in route
|
||||
export default withMiddlewares({
|
||||
GET: createAuthedProjectAPIRoute({
|
||||
name: "Get Scores",
|
||||
fn: async ({ auth }) => {
|
||||
// ❌ Direct ClickHouse query in route
|
||||
const scores = await queryClickhouse({
|
||||
query: "SELECT * FROM scores WHERE project_id = {projectId: String}",
|
||||
params: { projectId: auth.scope.projectId },
|
||||
});
|
||||
|
||||
return { data: scores };
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: Delegates to service or repository
|
||||
export default withMiddlewares({
|
||||
GET: createAuthedProjectAPIRoute({
|
||||
name: "Get Scores",
|
||||
fn: async ({ auth, query }) => {
|
||||
const scoresService = new ScoresApiService("v1");
|
||||
|
||||
return await scoresService.generateScoresForPublicApi({
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Anti-Pattern 3: Business Logic in Repositories
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Business logic in repository
|
||||
export const upsertScore = async (
|
||||
score: ScoreInsertType
|
||||
): Promise<void> => {
|
||||
// ❌ Validation in repository
|
||||
if (!score.name) {
|
||||
throw new Error("Score name is required");
|
||||
}
|
||||
|
||||
// ❌ Authorization check in repository
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: score.projectId },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
// ❌ Side effects in repository
|
||||
await auditLog({ ... });
|
||||
|
||||
await upsertClickhouse({ ... });
|
||||
};
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: Pure data access, no business logic
|
||||
export const upsertScore = async (
|
||||
score: ScoreInsertType
|
||||
): Promise<void> => {
|
||||
await upsertClickhouse({
|
||||
table: "scores",
|
||||
records: [score],
|
||||
eventBodyMapper: (body) => ({
|
||||
id: body.id,
|
||||
trace_id: body.traceId,
|
||||
name: body.name,
|
||||
value: body.value,
|
||||
}),
|
||||
tags: { feature: "scoring" },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
|
||||
- [SKILL.md](../SKILL.md) - Main backend development guidelines
|
||||
- [architecture-overview.md](architecture-overview.md) - System architecture
|
||||
- [middleware-guide.md](middleware-guide.md) - Middleware patterns
|
||||
- [database-patterns.md](database-patterns.md) - Database access patterns
|
||||
@@ -1,877 +0,0 @@
|
||||
# Services and Repositories - Business Logic Layer
|
||||
|
||||
Complete guide to organizing business logic with services and data access with repositories.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Service Layer Overview](#service-layer-overview)
|
||||
- [Dependency Injection Pattern](#dependency-injection-pattern)
|
||||
- [Singleton Pattern](#singleton-pattern)
|
||||
- [Repository Pattern](#repository-pattern)
|
||||
- [Service Design Principles](#service-design-principles)
|
||||
- [Caching Strategies](#caching-strategies)
|
||||
- [Testing Services](#testing-services)
|
||||
|
||||
---
|
||||
|
||||
## Service Layer Overview
|
||||
|
||||
### Purpose of Services
|
||||
|
||||
**Services contain business logic** - the 'what' and 'why' of your application:
|
||||
|
||||
```
|
||||
Controller asks: "Should I do this?"
|
||||
Service answers: "Yes/No, here's why, and here's what happens"
|
||||
Repository executes: "Here's the data you requested"
|
||||
```
|
||||
|
||||
**Services are responsible for:**
|
||||
|
||||
- ✅ Business rules enforcement
|
||||
- ✅ Orchestrating multiple repositories
|
||||
- ✅ Transaction management
|
||||
- ✅ Complex calculations
|
||||
- ✅ External service integration
|
||||
- ✅ Business validations
|
||||
|
||||
**Services should NOT:**
|
||||
|
||||
- ❌ Know about HTTP (Request/Response)
|
||||
- ❌ Direct Prisma access (use repositories)
|
||||
- ❌ Handle route-specific logic
|
||||
- ❌ Format HTTP responses
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection Pattern
|
||||
|
||||
### Why Dependency Injection?
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Easy to test (inject mocks)
|
||||
- Clear dependencies
|
||||
- Flexible configuration
|
||||
- Promotes loose coupling
|
||||
|
||||
### Excellent Example: NotificationService
|
||||
|
||||
**File:** `/blog-api/src/services/NotificationService.ts`
|
||||
|
||||
```typescript
|
||||
// Define dependencies interface for clarity
|
||||
export interface NotificationServiceDependencies {
|
||||
prisma: PrismaClient;
|
||||
batchingService: BatchingService;
|
||||
emailComposer: EmailComposer;
|
||||
}
|
||||
|
||||
// Service with dependency injection
|
||||
export class NotificationService {
|
||||
private prisma: PrismaClient;
|
||||
private batchingService: BatchingService;
|
||||
private emailComposer: EmailComposer;
|
||||
private preferencesCache: Map<
|
||||
string,
|
||||
{ preferences: UserPreference; timestamp: number }
|
||||
> = new Map();
|
||||
private CACHE_TTL =
|
||||
(notificationConfig.preferenceCacheTTLMinutes || 5) * 60 * 1000;
|
||||
|
||||
// Dependencies injected via constructor
|
||||
constructor(dependencies: NotificationServiceDependencies) {
|
||||
this.prisma = dependencies.prisma;
|
||||
this.batchingService = dependencies.batchingService;
|
||||
this.emailComposer = dependencies.emailComposer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification and route it appropriately
|
||||
*/
|
||||
async createNotification(params: CreateNotificationParams) {
|
||||
const {
|
||||
recipientID,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
link,
|
||||
context = {},
|
||||
channel = "both",
|
||||
priority = NotificationPriority.NORMAL,
|
||||
} = params;
|
||||
|
||||
try {
|
||||
// Get template and render content
|
||||
const template = getNotificationTemplate(type);
|
||||
const rendered = renderNotificationContent(template, context);
|
||||
|
||||
// Create in-app notification record
|
||||
const notificationId = await createNotificationRecord({
|
||||
instanceId: parseInt(context.instanceId || "0", 10),
|
||||
template: type,
|
||||
recipientUserId: recipientID,
|
||||
channel: channel === "email" ? "email" : "inApp",
|
||||
contextData: context,
|
||||
title: finalTitle,
|
||||
message: finalMessage,
|
||||
link: finalLink,
|
||||
});
|
||||
|
||||
// Route notification based on channel
|
||||
if (channel === "email" || channel === "both") {
|
||||
await this.routeNotification({
|
||||
notificationId,
|
||||
userId: recipientID,
|
||||
type,
|
||||
priority,
|
||||
title: finalTitle,
|
||||
message: finalMessage,
|
||||
link: finalLink,
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
return notification;
|
||||
} catch (error) {
|
||||
ErrorLogger.log(error, {
|
||||
context: {
|
||||
"[NotificationService] createNotification": {
|
||||
type: params.type,
|
||||
recipientID: params.recipientID,
|
||||
},
|
||||
},
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route notification based on user preferences
|
||||
*/
|
||||
private async routeNotification(params: {
|
||||
notificationId: number;
|
||||
userId: string;
|
||||
type: string;
|
||||
priority: NotificationPriority;
|
||||
title: string;
|
||||
message: string;
|
||||
link?: string;
|
||||
context?: Record<string, any>;
|
||||
}) {
|
||||
// Get user preferences with caching
|
||||
const preferences = await this.getUserPreferences(params.userId);
|
||||
|
||||
// Check if we should batch or send immediately
|
||||
if (this.shouldBatchEmail(preferences, params.type, params.priority)) {
|
||||
await this.batchingService.queueNotificationForBatch({
|
||||
notificationId: params.notificationId,
|
||||
userId: params.userId,
|
||||
userPreference: preferences,
|
||||
priority: params.priority,
|
||||
});
|
||||
} else {
|
||||
// Send immediately via EmailComposer
|
||||
await this.sendImmediateEmail({
|
||||
userId: params.userId,
|
||||
title: params.title,
|
||||
message: params.message,
|
||||
link: params.link,
|
||||
context: params.context,
|
||||
type: params.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if email should be batched
|
||||
*/
|
||||
shouldBatchEmail(
|
||||
preferences: UserPreference,
|
||||
notificationType: string,
|
||||
priority: NotificationPriority,
|
||||
): boolean {
|
||||
// HIGH priority always immediate
|
||||
if (priority === NotificationPriority.HIGH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check batch mode
|
||||
const batchMode = preferences.emailBatchMode || BatchMode.IMMEDIATE;
|
||||
return batchMode !== BatchMode.IMMEDIATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user preferences with caching
|
||||
*/
|
||||
async getUserPreferences(userId: string): Promise<UserPreference> {
|
||||
// Check cache first
|
||||
const cached = this.preferencesCache.get(userId);
|
||||
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
||||
return cached.preferences;
|
||||
}
|
||||
|
||||
const preference = await this.prisma.userPreference.findUnique({
|
||||
where: { userID: userId },
|
||||
});
|
||||
|
||||
const finalPreferences = preference || DEFAULT_PREFERENCES;
|
||||
|
||||
// Update cache
|
||||
this.preferencesCache.set(userId, {
|
||||
preferences: finalPreferences,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return finalPreferences;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Controller:**
|
||||
|
||||
```typescript
|
||||
// Instantiate with dependencies
|
||||
const notificationService = new NotificationService({
|
||||
prisma: PrismaService.main,
|
||||
batchingService: new BatchingService(PrismaService.main),
|
||||
emailComposer: new EmailComposer(),
|
||||
});
|
||||
|
||||
// Use in controller
|
||||
const notification = await notificationService.createNotification({
|
||||
recipientID: "user-123",
|
||||
type: "AFRLWorkflowNotification",
|
||||
context: { workflowName: "AFRL Monthly Report" },
|
||||
});
|
||||
```
|
||||
|
||||
**Key Takeaways:**
|
||||
|
||||
- Dependencies passed via constructor
|
||||
- Clear interface defines required dependencies
|
||||
- Easy to test (inject mocks)
|
||||
- Encapsulated caching logic
|
||||
- Business rules isolated from HTTP
|
||||
|
||||
---
|
||||
|
||||
## Singleton Pattern
|
||||
|
||||
### When to Use Singletons
|
||||
|
||||
**Use for:**
|
||||
|
||||
- Services with expensive initialization
|
||||
- Services with shared state (caching)
|
||||
- Services accessed from many places
|
||||
- Permission services
|
||||
- Configuration services
|
||||
|
||||
### Example: PermissionService (Singleton)
|
||||
|
||||
**File:** `/blog-api/src/services/permissionService.ts`
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
class PermissionService {
|
||||
private static instance: PermissionService;
|
||||
private prisma: PrismaClient;
|
||||
private permissionCache: Map<
|
||||
string,
|
||||
{ canAccess: boolean; timestamp: number }
|
||||
> = new Map();
|
||||
private CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Private constructor prevents direct instantiation
|
||||
private constructor() {
|
||||
this.prisma = PrismaService.main;
|
||||
}
|
||||
|
||||
// Get singleton instance
|
||||
public static getInstance(): PermissionService {
|
||||
if (!PermissionService.instance) {
|
||||
PermissionService.instance = new PermissionService();
|
||||
}
|
||||
return PermissionService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can complete a workflow step
|
||||
*/
|
||||
async canCompleteStep(
|
||||
userId: string,
|
||||
stepInstanceId: number,
|
||||
): Promise<boolean> {
|
||||
const cacheKey = `${userId}:${stepInstanceId}`;
|
||||
|
||||
// Check cache
|
||||
const cached = this.permissionCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
||||
return cached.canAccess;
|
||||
}
|
||||
|
||||
try {
|
||||
const post = await this.prisma.post.findUnique({
|
||||
where: { id: postId },
|
||||
include: {
|
||||
author: true,
|
||||
comments: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user has permission
|
||||
const canEdit =
|
||||
post.authorId === userId || (await this.isUserAdmin(userId));
|
||||
|
||||
// Cache result
|
||||
this.permissionCache.set(cacheKey, {
|
||||
canAccess: isAssigned,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return isAssigned;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[PermissionService] Error checking step permission:",
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for user
|
||||
*/
|
||||
clearUserCache(userId: string): void {
|
||||
for (const [key] of this.permissionCache) {
|
||||
if (key.startsWith(`${userId}:`)) {
|
||||
this.permissionCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.permissionCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const permissionService = PermissionService.getInstance();
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { permissionService } from "../services/permissionService";
|
||||
|
||||
// Use anywhere in the codebase
|
||||
const canComplete = await permissionService.canCompleteStep(userId, stepId);
|
||||
|
||||
if (!canComplete) {
|
||||
throw new ForbiddenError("You do not have permission to complete this step");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
### Purpose of Repositories
|
||||
|
||||
**Repositories abstract data access** - the 'how' of data operations:
|
||||
|
||||
```
|
||||
Service: "Get me all active users sorted by name"
|
||||
Repository: "Here's the Prisma query that does that"
|
||||
```
|
||||
|
||||
**Repositories are responsible for:**
|
||||
|
||||
- ✅ All Prisma operations
|
||||
- ✅ Query construction
|
||||
- ✅ Query optimization (select, include)
|
||||
- ✅ Database error handling
|
||||
- ✅ Caching database results
|
||||
|
||||
**Repositories should NOT:**
|
||||
|
||||
- ❌ Contain business logic
|
||||
- ❌ Know about HTTP
|
||||
- ❌ Make decisions (that's service layer)
|
||||
|
||||
### Repository Template
|
||||
|
||||
```typescript
|
||||
// repositories/UserRepository.ts
|
||||
import { PrismaService } from "@project-lifecycle-portal/database";
|
||||
import type { User, Prisma } from "@project-lifecycle-portal/database";
|
||||
|
||||
export class UserRepository {
|
||||
/**
|
||||
* Find user by ID with optimized query
|
||||
*/
|
||||
async findById(userId: string): Promise<User | null> {
|
||||
try {
|
||||
return await PrismaService.main.user.findUnique({
|
||||
where: { userID: userId },
|
||||
select: {
|
||||
userID: true,
|
||||
email: true,
|
||||
name: true,
|
||||
isActive: true,
|
||||
roles: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[UserRepository] Error finding user by ID:", error);
|
||||
throw new Error(`Failed to find user: ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all active users
|
||||
*/
|
||||
async findActive(options?: {
|
||||
orderBy?: Prisma.UserOrderByWithRelationInput;
|
||||
}): Promise<User[]> {
|
||||
try {
|
||||
return await PrismaService.main.user.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: options?.orderBy || { name: "asc" },
|
||||
select: {
|
||||
userID: true,
|
||||
email: true,
|
||||
name: true,
|
||||
roles: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[UserRepository] Error finding active users:", error);
|
||||
throw new Error("Failed to find active users");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by email
|
||||
*/
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
try {
|
||||
return await PrismaService.main.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[UserRepository] Error finding user by email:", error);
|
||||
throw new Error(`Failed to find user with email: ${email}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new user
|
||||
*/
|
||||
async create(data: Prisma.UserCreateInput): Promise<User> {
|
||||
try {
|
||||
return await PrismaService.main.user.create({ data });
|
||||
} catch (error) {
|
||||
console.error("[UserRepository] Error creating user:", error);
|
||||
throw new Error("Failed to create user");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user
|
||||
*/
|
||||
async update(userId: string, data: Prisma.UserUpdateInput): Promise<User> {
|
||||
try {
|
||||
return await PrismaService.main.user.update({
|
||||
where: { userID: userId },
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[UserRepository] Error updating user:", error);
|
||||
throw new Error(`Failed to update user: ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user (soft delete by setting isActive = false)
|
||||
*/
|
||||
async delete(userId: string): Promise<User> {
|
||||
try {
|
||||
return await PrismaService.main.user.update({
|
||||
where: { userID: userId },
|
||||
data: { isActive: false },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[UserRepository] Error deleting user:", error);
|
||||
throw new Error(`Failed to delete user: ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if email exists
|
||||
*/
|
||||
async emailExists(email: string): Promise<boolean> {
|
||||
try {
|
||||
const count = await PrismaService.main.user.count({
|
||||
where: { email },
|
||||
});
|
||||
return count > 0;
|
||||
} catch (error) {
|
||||
console.error("[UserRepository] Error checking email exists:", error);
|
||||
throw new Error("Failed to check if email exists");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const userRepository = new UserRepository();
|
||||
```
|
||||
|
||||
**Using Repository in Service:**
|
||||
|
||||
```typescript
|
||||
// services/userService.ts
|
||||
import { userRepository } from "../repositories/UserRepository";
|
||||
import { ConflictError, NotFoundError } from "../utils/errors";
|
||||
|
||||
export class UserService {
|
||||
/**
|
||||
* Create new user with business rules
|
||||
*/
|
||||
async createUser(data: {
|
||||
email: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
}): Promise<User> {
|
||||
// Business rule: Check if email already exists
|
||||
const emailExists = await userRepository.emailExists(data.email);
|
||||
if (emailExists) {
|
||||
throw new ConflictError("Email already exists");
|
||||
}
|
||||
|
||||
// Business rule: Validate roles
|
||||
const validRoles = ["admin", "operations", "user"];
|
||||
const invalidRoles = data.roles.filter(
|
||||
(role) => !validRoles.includes(role),
|
||||
);
|
||||
if (invalidRoles.length > 0) {
|
||||
throw new ValidationError(`Invalid roles: ${invalidRoles.join(", ")}`);
|
||||
}
|
||||
|
||||
// Create user via repository
|
||||
return await userRepository.create({
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
roles: data.roles,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID
|
||||
*/
|
||||
async getUser(userId: string): Promise<User> {
|
||||
const user = await userRepository.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError(`User not found: ${userId}`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Design Principles
|
||||
|
||||
### 1. Single Responsibility
|
||||
|
||||
Each service should have ONE clear purpose:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Single responsibility
|
||||
class UserService {
|
||||
async createUser() {}
|
||||
async updateUser() {}
|
||||
async deleteUser() {}
|
||||
}
|
||||
|
||||
class EmailService {
|
||||
async sendEmail() {}
|
||||
async sendBulkEmails() {}
|
||||
}
|
||||
|
||||
// ❌ BAD - Too many responsibilities
|
||||
class UserService {
|
||||
async createUser() {}
|
||||
async sendWelcomeEmail() {} // Should be EmailService
|
||||
async logUserActivity() {} // Should be AuditService
|
||||
async processPayment() {} // Should be PaymentService
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Clear Method Names
|
||||
|
||||
Method names should describe WHAT they do:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Clear intent
|
||||
async createNotification()
|
||||
async getUserPreferences()
|
||||
async shouldBatchEmail()
|
||||
async routeNotification()
|
||||
|
||||
// ❌ BAD - Vague or misleading
|
||||
async process()
|
||||
async handle()
|
||||
async doIt()
|
||||
async execute()
|
||||
```
|
||||
|
||||
### 3. Use Params Objects for Multiple Arguments
|
||||
|
||||
When a function receives multiple arguments, use a single params object instead of positional arguments:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Positional arguments are unclear and can be swapped
|
||||
async function createTrace(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
name: string,
|
||||
) {}
|
||||
|
||||
// Call site - which string is which?
|
||||
await createTrace(projectId, userId, sessionId, name);
|
||||
|
||||
// ✅ GOOD - Params object makes intent clear
|
||||
async function createTrace(params: {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
name: string;
|
||||
}) {}
|
||||
|
||||
// Call site - clear and prevents argument swapping bugs
|
||||
await createTrace({ projectId, userId, sessionId, name });
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- More readable at call sites
|
||||
- Prevents bugs when positional arguments of the same type are accidentally swapped
|
||||
- Easier to add optional parameters later
|
||||
- Self-documenting code
|
||||
|
||||
### 4. Return Types
|
||||
|
||||
Always use explicit return types:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Explicit types
|
||||
async createUser(data: CreateUserDTO): Promise<User> {}
|
||||
async findUsers(): Promise<User[]> {}
|
||||
async deleteUser(id: string): Promise<void> {}
|
||||
|
||||
// ❌ BAD - Implicit any
|
||||
async createUser(data) {} // No types!
|
||||
```
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
Services should throw meaningful errors:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Meaningful errors
|
||||
if (!user) {
|
||||
throw new NotFoundError(`User not found: ${userId}`);
|
||||
}
|
||||
|
||||
if (emailExists) {
|
||||
throw new ConflictError("Email already exists");
|
||||
}
|
||||
|
||||
// ❌ BAD - Generic errors
|
||||
if (!user) {
|
||||
throw new Error("Error"); // What error?
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Avoid God Services
|
||||
|
||||
Don't create services that do everything:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - God service
|
||||
class WorkflowService {
|
||||
async startWorkflow() {}
|
||||
async completeStep() {}
|
||||
async assignRoles() {}
|
||||
async sendNotifications() {} // Should be NotificationService
|
||||
async validatePermissions() {} // Should be PermissionService
|
||||
async logAuditTrail() {} // Should be AuditService
|
||||
// ... 50 more methods
|
||||
}
|
||||
|
||||
// ✅ GOOD - Focused services
|
||||
class WorkflowService {
|
||||
constructor(
|
||||
private notificationService: NotificationService,
|
||||
private permissionService: PermissionService,
|
||||
private auditService: AuditService,
|
||||
) {}
|
||||
|
||||
async startWorkflow() {
|
||||
// Orchestrate other services
|
||||
await this.permissionService.checkPermission();
|
||||
await this.workflowRepository.create();
|
||||
await this.notificationService.notify();
|
||||
await this.auditService.log();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### 1. In-Memory Caching
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
private cache: Map<string, { user: User; timestamp: number }> = new Map();
|
||||
private CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
async getUser(userId: string): Promise<User> {
|
||||
// Check cache
|
||||
const cached = this.cache.get(userId);
|
||||
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
||||
return cached.user;
|
||||
}
|
||||
|
||||
// Fetch from database
|
||||
const user = await userRepository.findById(userId);
|
||||
|
||||
// Update cache
|
||||
if (user) {
|
||||
this.cache.set(userId, { user, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
clearUserCache(userId: string): void {
|
||||
this.cache.delete(userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Cache Invalidation
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
async updateUser(userId: string, data: UpdateUserDTO): Promise<User> {
|
||||
// Update in database
|
||||
const user = await userRepository.update(userId, data);
|
||||
|
||||
// Invalidate cache
|
||||
this.clearUserCache(userId);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Services
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```typescript
|
||||
// tests/userService.test.ts
|
||||
import { UserService } from "../services/userService";
|
||||
import { userRepository } from "../repositories/UserRepository";
|
||||
import { ConflictError } from "../utils/errors";
|
||||
|
||||
// Mock repository
|
||||
jest.mock("../repositories/UserRepository");
|
||||
|
||||
describe("UserService", () => {
|
||||
let userService: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
userService = new UserService();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("should create user when email does not exist", async () => {
|
||||
// Arrange
|
||||
const userData = {
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
roles: ["user"],
|
||||
};
|
||||
|
||||
(userRepository.emailExists as jest.Mock).mockResolvedValue(false);
|
||||
(userRepository.create as jest.Mock).mockResolvedValue({
|
||||
userID: "123",
|
||||
...userData,
|
||||
});
|
||||
|
||||
// Act
|
||||
const user = await userService.createUser(userData);
|
||||
|
||||
// Assert
|
||||
expect(user).toBeDefined();
|
||||
expect(user.email).toBe(userData.email);
|
||||
expect(userRepository.emailExists).toHaveBeenCalledWith(userData.email);
|
||||
expect(userRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw ConflictError when email exists", async () => {
|
||||
// Arrange
|
||||
const userData = {
|
||||
email: "existing@example.com",
|
||||
name: "Test User",
|
||||
roles: ["user"],
|
||||
};
|
||||
|
||||
(userRepository.emailExists as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
// Act & Assert
|
||||
await expect(userService.createUser(userData)).rejects.toThrow(
|
||||
ConflictError,
|
||||
);
|
||||
expect(userRepository.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
|
||||
- [SKILL.md](SKILL.md) - Main guide
|
||||
- [routing-and-controllers.md](routing-and-controllers.md) - Controllers that use services
|
||||
- [database-patterns.md](database-patterns.md) - Prisma and repository patterns
|
||||
- [complete-examples.md](complete-examples.md) - Full service/repository examples
|
||||
@@ -1,558 +0,0 @@
|
||||
# Testing Guide - Backend Testing Strategies
|
||||
|
||||
Complete guide to testing Langfuse backend services across web, worker, and shared packages.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Test Types Overview](#test-types-overview)
|
||||
- [Integration Tests (Public API)](#integration-tests-public-api)
|
||||
- [Service-Level Tests (Repository/Service)](#service-level-tests-repositoryservice)
|
||||
- [tRPC Tests (Procedure Testing)](#trpc-tests-procedure-testing)
|
||||
- [Worker Tests (Queue Processing)](#worker-tests-queue-processing)
|
||||
- [Key Testing Principles](#key-testing-principles)
|
||||
- [Running Tests](#running-tests)
|
||||
|
||||
---
|
||||
|
||||
## Test Types Overview
|
||||
|
||||
Langfuse uses multiple testing strategies for different layers:
|
||||
|
||||
| Test Type | Framework | Location | Purpose |
|
||||
|-----------|-----------|----------|---------|
|
||||
| Integration | Jest | `web/src/__tests__/async/` | Full API endpoint testing |
|
||||
| tRPC | Jest | `web/src/__tests__/async/` | tRPC procedure testing with auth |
|
||||
| Service | Jest | `web/src/__tests__/async/repositories/` | Repository/service function testing |
|
||||
| Worker | Vitest | `worker/src/__tests__/` | Queue processors and streams |
|
||||
|
||||
---
|
||||
|
||||
## Integration Tests (Public API)
|
||||
|
||||
Test full REST API endpoints end-to-end using HTTP requests.
|
||||
|
||||
**File location:** `web/src/__tests__/async/datasets-api.servertest.ts`
|
||||
|
||||
```typescript
|
||||
import { makeZodVerifiedAPICall } from "../helpers";
|
||||
import { PostDatasetsV1Response } from "@/src/features/public-api/types/datasets";
|
||||
|
||||
describe("Dataset API", () => {
|
||||
it("should create dataset", async () => {
|
||||
const res = await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{ name: "test-dataset" },
|
||||
auth,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("should validate input", async () => {
|
||||
const res = await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{ name: "" }, // Invalid empty name
|
||||
auth,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Uses `makeZodVerifiedAPICall` for type-safe API testing
|
||||
- Tests HTTP status codes and response validation
|
||||
- Tests both success and error cases
|
||||
|
||||
---
|
||||
|
||||
## Service-Level Tests (Repository/Service)
|
||||
|
||||
Test individual repository/service functions with isolated data.
|
||||
|
||||
**File location:** `web/src/__tests__/async/repositories/event-repository.servertest.ts`
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createEvent,
|
||||
createEventsCh,
|
||||
getObservationsWithModelDataFromEventsTable,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
describe("Event Repository Tests", () => {
|
||||
it("should return observations with model data", async () => {
|
||||
const traceId = randomUUID();
|
||||
const generationId = randomUUID();
|
||||
const modelId = randomUUID();
|
||||
|
||||
// Create test data
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: modelId,
|
||||
projectId,
|
||||
modelName: `gpt-4-${modelId}`,
|
||||
matchPattern: `(?i)^(gpt-?4-${modelId})$`,
|
||||
startDate: new Date("2023-01-01"),
|
||||
unit: "TOKENS",
|
||||
Price: {
|
||||
create: [
|
||||
{ usageType: "input", price: 0.03 },
|
||||
{ usageType: "output", price: 0.06 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const event = createEvent({
|
||||
id: generationId,
|
||||
span_id: generationId,
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
type: "GENERATION",
|
||||
name: `test-generation-${generationId}`,
|
||||
model_id: modelId,
|
||||
});
|
||||
|
||||
await createEventsCh([event]);
|
||||
|
||||
// Test the service function
|
||||
const result = await getObservationsWithModelDataFromEventsTable({
|
||||
projectId,
|
||||
filter: [{ type: "string", column: "id", operator: "=", value: generationId }],
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
const observation = result.find((o) => o.id === generationId);
|
||||
expect(observation?.internalModelId).toBe(modelId);
|
||||
expect(Number(observation?.inputPrice)).toBeCloseTo(0.03, 5);
|
||||
|
||||
// Cleanup
|
||||
await prisma.model.delete({ where: { id: modelId } });
|
||||
});
|
||||
|
||||
it("should handle filters correctly", async () => {
|
||||
const projectId = randomUUID();
|
||||
const traceId = randomUUID();
|
||||
|
||||
const observations = [
|
||||
createEvent({
|
||||
id: randomUUID(),
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
type: "GENERATION",
|
||||
name: "test1",
|
||||
}),
|
||||
createEvent({
|
||||
id: randomUUID(),
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
type: "SPAN",
|
||||
name: "test2",
|
||||
}),
|
||||
];
|
||||
|
||||
await createEventsCh(observations);
|
||||
|
||||
const result = await getObservationsWithModelDataFromEventsTable({
|
||||
projectId,
|
||||
filter: [
|
||||
{ type: "stringOptions", column: "type", operator: "any of", value: ["GENERATION"] }
|
||||
],
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(result.every(o => o.type === "GENERATION")).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Tests service/repository functions directly
|
||||
- Uses ClickHouse and Prisma test data
|
||||
- Always cleanup test data after tests
|
||||
- Use unique IDs to avoid test interference
|
||||
|
||||
---
|
||||
|
||||
## tRPC Tests (Procedure Testing)
|
||||
|
||||
Test tRPC procedures with caller pattern and auth context.
|
||||
|
||||
**File location:** `web/src/__tests__/async/automations-trpc.servertest.ts`
|
||||
|
||||
```typescript
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { createOrgProjectAndApiKey } from "@langfuse/shared/src/server";
|
||||
import type { Session } from "next-auth";
|
||||
import { v4 } from "uuid";
|
||||
import { JobConfigState } from "@langfuse/shared";
|
||||
|
||||
async function prepare() {
|
||||
const { project, org } = await createOrgProjectAndApiKey();
|
||||
|
||||
const session: Session = {
|
||||
expires: "1",
|
||||
user: {
|
||||
id: "user-1",
|
||||
name: "Demo User",
|
||||
organizations: [{
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
role: "OWNER",
|
||||
projects: [{
|
||||
id: project.id,
|
||||
role: "ADMIN",
|
||||
name: project.name,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createInnerTRPCContext({ session, headers: {} });
|
||||
const caller = appRouter.createCaller({ ...ctx, prisma });
|
||||
|
||||
return { project, org, session, ctx, caller };
|
||||
}
|
||||
|
||||
describe("automations trpc", () => {
|
||||
it("should retrieve all automations for a project", async () => {
|
||||
const { project, caller } = await prepare();
|
||||
|
||||
// Create test trigger
|
||||
const trigger = await prisma.trigger.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
projectId: project.id,
|
||||
eventSource: "prompt",
|
||||
eventActions: ["created"],
|
||||
filter: [],
|
||||
status: JobConfigState.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
// Create test action
|
||||
const action = await prisma.action.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
projectId: project.id,
|
||||
type: "WEBHOOK",
|
||||
config: {
|
||||
type: "WEBHOOK",
|
||||
url: "https://example.com/webhook",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Link trigger to action
|
||||
await prisma.automation.create({
|
||||
data: {
|
||||
projectId: project.id,
|
||||
triggerId: trigger.id,
|
||||
actionId: action.id,
|
||||
name: "Test Automation",
|
||||
},
|
||||
});
|
||||
|
||||
// Call tRPC procedure
|
||||
const response = await caller.automations.getAutomations({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response[0]).toMatchObject({
|
||||
name: "Test Automation",
|
||||
trigger: expect.objectContaining({
|
||||
id: trigger.id,
|
||||
eventSource: "prompt",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error when user lacks permissions", async () => {
|
||||
const { project, session } = await prepare();
|
||||
|
||||
// Create limited session
|
||||
const limitedSession: Session = {
|
||||
...session,
|
||||
user: {
|
||||
...session.user!,
|
||||
organizations: [{
|
||||
...session.user!.organizations[0],
|
||||
projects: [{
|
||||
...session.user!.organizations[0].projects[0],
|
||||
role: "VIEWER", // VIEWER can't create automations
|
||||
}],
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
const limitedCtx = createInnerTRPCContext({
|
||||
session: limitedSession,
|
||||
headers: {},
|
||||
});
|
||||
const limitedCaller = appRouter.createCaller({ ...limitedCtx, prisma });
|
||||
|
||||
await expect(
|
||||
limitedCaller.automations.createAutomation({
|
||||
projectId: project.id,
|
||||
name: "Unauthorized",
|
||||
eventSource: "prompt",
|
||||
eventAction: ["created"],
|
||||
filter: [],
|
||||
status: JobConfigState.ACTIVE,
|
||||
actionType: "WEBHOOK",
|
||||
actionConfig: {
|
||||
type: "WEBHOOK",
|
||||
url: "https://example.com/webhook",
|
||||
requestHeaders: {},
|
||||
apiVersion: { prompt: "v1" },
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("User does not have access");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Uses `prepare()` helper to set up test context
|
||||
- Creates authenticated caller with `appRouter.createCaller`
|
||||
- Tests both success and permission error cases
|
||||
- Can test different user roles and permissions
|
||||
|
||||
---
|
||||
|
||||
## Worker Tests (Queue Processing)
|
||||
|
||||
Test queue processors and stream functions using vitest.
|
||||
|
||||
**File location:** `worker/src/__tests__/batchExport.test.ts`
|
||||
|
||||
```typescript
|
||||
import { randomUUID } from "crypto";
|
||||
import { expect, describe, it } from "vitest";
|
||||
import {
|
||||
createObservation,
|
||||
createObservationsCh,
|
||||
createOrgProjectAndApiKey,
|
||||
createTraceScore,
|
||||
createScoresCh,
|
||||
createTrace,
|
||||
createTracesCh,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { getObservationStream } from "../features/database-read-stream/observation-stream";
|
||||
|
||||
describe("batch export test suite", () => {
|
||||
it("should export observations", async () => {
|
||||
const { projectId } = await createOrgProjectAndApiKey();
|
||||
|
||||
const traceId = randomUUID();
|
||||
const trace = createTrace({
|
||||
project_id: projectId,
|
||||
id: traceId,
|
||||
});
|
||||
|
||||
await createTracesCh([trace]);
|
||||
|
||||
const observations = [
|
||||
createObservation({
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
type: "SPAN",
|
||||
}),
|
||||
createObservation({
|
||||
project_id: projectId,
|
||||
trace_id: randomUUID(),
|
||||
type: "GENERATION",
|
||||
}),
|
||||
];
|
||||
|
||||
const score = createTraceScore({
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
observation_id: observations[0].id,
|
||||
name: "test",
|
||||
value: 123,
|
||||
});
|
||||
|
||||
await createScoresCh([score]);
|
||||
await createObservationsCh(observations);
|
||||
|
||||
// Test the stream function
|
||||
const stream = await getObservationStream({
|
||||
projectId: projectId,
|
||||
cutoffCreatedAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||
filter: [],
|
||||
});
|
||||
|
||||
const rows: any[] = [];
|
||||
for await (const chunk of stream) {
|
||||
rows.push(chunk);
|
||||
}
|
||||
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: observations[0].id,
|
||||
type: observations[0].type,
|
||||
test: [score.value],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should export with filters", async () => {
|
||||
const { projectId } = await createOrgProjectAndApiKey();
|
||||
|
||||
const observations = [
|
||||
createObservation({
|
||||
project_id: projectId,
|
||||
trace_id: randomUUID(),
|
||||
type: "GENERATION",
|
||||
name: "test1",
|
||||
}),
|
||||
createObservation({
|
||||
project_id: projectId,
|
||||
trace_id: randomUUID(),
|
||||
type: "SPAN",
|
||||
name: "test2",
|
||||
}),
|
||||
];
|
||||
|
||||
await createObservationsCh(observations);
|
||||
|
||||
const stream = await getObservationStream({
|
||||
projectId: projectId,
|
||||
cutoffCreatedAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||
filter: [
|
||||
{
|
||||
type: "stringOptions",
|
||||
operator: "any of",
|
||||
column: "name",
|
||||
value: ["test1"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const rows: any[] = [];
|
||||
for await (const chunk of stream) {
|
||||
rows.push(chunk);
|
||||
}
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].name).toBe("test1");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Uses vitest (not Jest) for worker tests
|
||||
- Tests stream functions with async iteration
|
||||
- Creates isolated test data per test
|
||||
- Use unique project IDs to avoid interference
|
||||
|
||||
---
|
||||
|
||||
## Key Testing Principles
|
||||
|
||||
### General Principles
|
||||
|
||||
1. **Test Isolation**: Each test should be independent and runnable in any order
|
||||
2. **Unique IDs**: Use `randomUUID()` or unique project IDs to avoid test interference
|
||||
3. **Cleanup**: Always clean up test data in service tests (or use unique project IDs)
|
||||
4. **No `pruneDatabase`**: Avoid `pruneDatabase` calls, especially in `__tests__/async/` directory
|
||||
|
||||
### By Test Type
|
||||
|
||||
| Test Type | Key Principles |
|
||||
|-----------|----------------|
|
||||
| **Integration** | Test HTTP endpoints, validate status codes and response shapes |
|
||||
| **tRPC** | Use `createInnerTRPCContext` and `appRouter.createCaller`, test auth/permissions |
|
||||
| **Service** | Test individual functions with isolated data, always cleanup |
|
||||
| **Worker** | Use vitest, test streams with async iteration, test filtering logic |
|
||||
|
||||
### Test Data Management
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: Use unique IDs
|
||||
const projectId = randomUUID();
|
||||
const traceId = randomUUID();
|
||||
|
||||
// ✅ GOOD: Cleanup in service tests
|
||||
afterAll(async () => {
|
||||
await prisma.model.delete({ where: { id: modelId } });
|
||||
});
|
||||
|
||||
// ✅ GOOD: Use unique projects (no cleanup needed)
|
||||
const { projectId } = await createOrgProjectAndApiKey();
|
||||
|
||||
// ❌ BAD: Shared test data between tests
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
|
||||
// ❌ BAD: Using pruneDatabase
|
||||
await pruneDatabase();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Web Tests (Jest)
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run sync tests
|
||||
pnpm test-sync
|
||||
|
||||
# Run async tests
|
||||
pnpm test -- --testPathPattern="async"
|
||||
|
||||
# Run specific test file
|
||||
pnpm test -- --testPathPattern="datasets-api"
|
||||
|
||||
# Run specific test
|
||||
pnpm test -- --testPathPattern="datasets-api" --testNamePattern="should create dataset"
|
||||
```
|
||||
|
||||
### Worker Tests (Vitest)
|
||||
|
||||
```bash
|
||||
# Run all worker tests
|
||||
pnpm run test --filter=worker
|
||||
|
||||
# Run specific test file
|
||||
pnpm run test --filter=worker -- batchExport
|
||||
|
||||
# Run specific test
|
||||
pnpm run test --filter=worker -- batchExport -t "should export observations"
|
||||
```
|
||||
|
||||
### Coverage
|
||||
|
||||
```bash
|
||||
# Web coverage
|
||||
pnpm test -- --coverage
|
||||
|
||||
# Worker coverage
|
||||
pnpm run test --filter=worker -- --coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
- [SKILL.md](../SKILL.md) - Main backend guidelines
|
||||
- [architecture-overview.md](architecture-overview.md) - Architecture patterns
|
||||
- [complete-examples.md](complete-examples.md) - Full code examples
|
||||
@@ -1,197 +0,0 @@
|
||||
# Advanced Topics & Future Enhancements
|
||||
|
||||
Ideas and concepts for future improvements to the skill system.
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Rule Updates
|
||||
|
||||
**Current State:** Requires Claude Code restart to pick up changes to skill-rules.json
|
||||
|
||||
**Future Enhancement:** Hot-reload configuration without restart
|
||||
|
||||
**Implementation Ideas:**
|
||||
- Watch skill-rules.json for changes
|
||||
- Reload on file modification
|
||||
- Invalidate cached compiled regexes
|
||||
- Notify user of reload
|
||||
|
||||
**Benefits:**
|
||||
- Faster iteration during skill development
|
||||
- No need to restart Claude Code
|
||||
- Better developer experience
|
||||
|
||||
---
|
||||
|
||||
## Skill Dependencies
|
||||
|
||||
**Current State:** Skills are independent
|
||||
|
||||
**Future Enhancement:** Specify skill dependencies and load order
|
||||
|
||||
**Configuration Idea:**
|
||||
```json
|
||||
{
|
||||
"my-advanced-skill": {
|
||||
"dependsOn": ["prerequisite-skill", "base-skill"],
|
||||
"type": "domain",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Advanced skill builds on base skill knowledge
|
||||
- Ensure foundational skills loaded first
|
||||
- Chain skills for complex workflows
|
||||
|
||||
**Benefits:**
|
||||
- Better skill composition
|
||||
- Clearer skill relationships
|
||||
- Progressive disclosure
|
||||
|
||||
---
|
||||
|
||||
## Conditional Enforcement
|
||||
|
||||
**Current State:** Enforcement level is static
|
||||
|
||||
**Future Enhancement:** Enforce based on context or environment
|
||||
|
||||
**Configuration Idea:**
|
||||
```json
|
||||
{
|
||||
"enforcement": {
|
||||
"default": "suggest",
|
||||
"when": {
|
||||
"production": "block",
|
||||
"development": "suggest",
|
||||
"ci": "block"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Stricter enforcement in production
|
||||
- Relaxed rules during development
|
||||
- CI/CD pipeline requirements
|
||||
|
||||
**Benefits:**
|
||||
- Environment-appropriate enforcement
|
||||
- Flexible rule application
|
||||
- Context-aware guardrails
|
||||
|
||||
---
|
||||
|
||||
## Skill Analytics
|
||||
|
||||
**Current State:** No usage tracking
|
||||
|
||||
**Future Enhancement:** Track skill usage patterns and effectiveness
|
||||
|
||||
**Metrics to Collect:**
|
||||
- Skill trigger frequency
|
||||
- False positive rate
|
||||
- False negative rate
|
||||
- Time to skill usage after suggestion
|
||||
- User override rate (skip markers, env vars)
|
||||
- Performance metrics (execution time)
|
||||
|
||||
**Dashbord Ideas:**
|
||||
- Most/least used skills
|
||||
- Skills with highest false positive rate
|
||||
- Performance bottlenecks
|
||||
- Skill effectiveness scores
|
||||
|
||||
**Benefits:**
|
||||
- Data-driven skill improvement
|
||||
- Identify problems early
|
||||
- Optimize patterns based on real usage
|
||||
|
||||
---
|
||||
|
||||
## Skill Versioning
|
||||
|
||||
**Current State:** No version tracking
|
||||
|
||||
**Future Enhancement:** Version skills and track compatibility
|
||||
|
||||
**Configuration Idea:**
|
||||
```json
|
||||
{
|
||||
"my-skill": {
|
||||
"version": "2.1.0",
|
||||
"minClaudeVersion": "1.5.0",
|
||||
"changelog": "Added support for new workflow patterns",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Track skill evolution
|
||||
- Ensure compatibility
|
||||
- Document changes
|
||||
- Support migration paths
|
||||
|
||||
---
|
||||
|
||||
## Multi-Language Support
|
||||
|
||||
**Current State:** English only
|
||||
|
||||
**Future Enhancement:** Support multiple languages for skill content
|
||||
|
||||
**Implementation Ideas:**
|
||||
- Language-specific SKILL.md variants
|
||||
- Automatic language detection
|
||||
- Fallback to English
|
||||
|
||||
**Use Cases:**
|
||||
- International teams
|
||||
- Localized documentation
|
||||
- Multi-language projects
|
||||
|
||||
---
|
||||
|
||||
## Skill Testing Framework
|
||||
|
||||
**Current State:** Manual testing with npx tsx commands
|
||||
|
||||
**Future Enhancement:** Automated skill testing
|
||||
|
||||
**Features:**
|
||||
- Test cases for trigger patterns
|
||||
- Assertion framework
|
||||
- CI/CD integration
|
||||
- Coverage reports
|
||||
|
||||
**Example Test:**
|
||||
```typescript
|
||||
describe('database-verification', () => {
|
||||
it('triggers on Prisma imports', () => {
|
||||
const result = testSkill({
|
||||
prompt: "add user tracking",
|
||||
file: "services/user.ts",
|
||||
content: "import { PrismaService } from './prisma'"
|
||||
});
|
||||
|
||||
expect(result.triggered).toBe(true);
|
||||
expect(result.skill).toBe('database-verification');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Prevent regressions
|
||||
- Validate patterns before deployment
|
||||
- Confidence in changes
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
- [SKILL.md](SKILL.md) - Main skill guide
|
||||
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Current debugging guide
|
||||
- [HOOK_MECHANISMS.md](HOOK_MECHANISMS.md) - How hooks work today
|
||||
@@ -1,306 +0,0 @@
|
||||
# Hook Mechanisms - Deep Dive
|
||||
|
||||
Technical deep dive into how the UserPromptSubmit and PreToolUse hooks work.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [UserPromptSubmit Hook Flow](#userpromptsubmit-hook-flow)
|
||||
- [PreToolUse Hook Flow](#pretooluse-hook-flow)
|
||||
- [Exit Code Behavior (CRITICAL)](#exit-code-behavior-critical)
|
||||
- [Session State Management](#session-state-management)
|
||||
- [Performance Considerations](#performance-considerations)
|
||||
|
||||
---
|
||||
|
||||
## UserPromptSubmit Hook Flow
|
||||
|
||||
### Execution Sequence
|
||||
|
||||
```
|
||||
User submits prompt
|
||||
↓
|
||||
.claude/settings.json registers hook
|
||||
↓
|
||||
skill-activation-prompt.sh executes
|
||||
↓
|
||||
npx tsx skill-activation-prompt.ts
|
||||
↓
|
||||
Hook reads stdin (JSON with prompt)
|
||||
↓
|
||||
Loads skill-rules.json
|
||||
↓
|
||||
Matches keywords + intent patterns
|
||||
↓
|
||||
Groups matches by priority (critical → high → medium → low)
|
||||
↓
|
||||
Outputs formatted message to stdout
|
||||
↓
|
||||
stdout becomes context for Claude (injected before prompt)
|
||||
↓
|
||||
Claude sees: [skill suggestion] + user's prompt
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
- **Exit code**: Always 0 (allow)
|
||||
- **stdout**: → Claude's context (injected as system message)
|
||||
- **Timing**: Runs BEFORE Claude processes prompt
|
||||
- **Behavior**: Non-blocking, advisory only
|
||||
- **Purpose**: Make Claude aware of relevant skills
|
||||
|
||||
### Input Format
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc-123",
|
||||
"transcript_path": "/path/to/transcript.json",
|
||||
"cwd": "/root/git/your-project",
|
||||
"permission_mode": "normal",
|
||||
"hook_event_name": "UserPromptSubmit",
|
||||
"prompt": "how does the layout system work?"
|
||||
}
|
||||
```
|
||||
|
||||
### Output Format (to stdout)
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🎯 SKILL ACTIVATION CHECK
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📚 RECOMMENDED SKILLS:
|
||||
→ project-catalog-developer
|
||||
|
||||
ACTION: Use Skill tool BEFORE responding
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
Claude sees this output as additional context before processing the user's prompt.
|
||||
|
||||
---
|
||||
|
||||
## PreToolUse Hook Flow
|
||||
|
||||
### Execution Sequence
|
||||
|
||||
```
|
||||
Claude calls Edit/Write tool
|
||||
↓
|
||||
.claude/settings.json registers hook (matcher: Edit|Write)
|
||||
↓
|
||||
skill-verification-guard.sh executes
|
||||
↓
|
||||
npx tsx skill-verification-guard.ts
|
||||
↓
|
||||
Hook reads stdin (JSON with tool_name, tool_input)
|
||||
↓
|
||||
Loads skill-rules.json
|
||||
↓
|
||||
Checks file path patterns (glob matching)
|
||||
↓
|
||||
Reads file for content patterns (if file exists)
|
||||
↓
|
||||
Checks session state (was skill already used?)
|
||||
↓
|
||||
Checks skip conditions (file markers, env vars)
|
||||
↓
|
||||
IF MATCHED AND NOT SKIPPED:
|
||||
Update session state (mark skill as enforced)
|
||||
Output block message to stderr
|
||||
Exit with code 2 (BLOCK)
|
||||
ELSE:
|
||||
Exit with code 0 (ALLOW)
|
||||
↓
|
||||
IF BLOCKED:
|
||||
stderr → Claude sees message
|
||||
Edit/Write tool does NOT execute
|
||||
Claude must use skill and retry
|
||||
IF ALLOWED:
|
||||
Tool executes normally
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
- **Exit code 2**: BLOCK (stderr → Claude)
|
||||
- **Exit code 0**: ALLOW
|
||||
- **Timing**: Runs BEFORE tool execution
|
||||
- **Session tracking**: Prevents repeated blocks in same session
|
||||
- **Fail open**: On errors, allows operation (don't break workflow)
|
||||
- **Purpose**: Enforce critical guardrails
|
||||
|
||||
### Input Format
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc-123",
|
||||
"transcript_path": "/path/to/transcript.json",
|
||||
"cwd": "/root/git/your-project",
|
||||
"permission_mode": "normal",
|
||||
"hook_event_name": "PreToolUse",
|
||||
"tool_name": "Edit",
|
||||
"tool_input": {
|
||||
"file_path": "/root/git/your-project/form/src/services/user.ts",
|
||||
"old_string": "...",
|
||||
"new_string": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output Format (to stderr when blocked)
|
||||
|
||||
```
|
||||
⚠️ BLOCKED - Database Operation Detected
|
||||
|
||||
📋 REQUIRED ACTION:
|
||||
1. Use Skill tool: 'database-verification'
|
||||
2. Verify ALL table and column names against schema
|
||||
3. Check database structure with DESCRIBE commands
|
||||
4. Then retry this edit
|
||||
|
||||
Reason: Prevent column name errors in Prisma queries
|
||||
File: form/src/services/user.ts
|
||||
|
||||
💡 TIP: Add '// @skip-validation' comment to skip future checks
|
||||
```
|
||||
|
||||
Claude receives this message and understands it needs to use the skill before retrying the edit.
|
||||
|
||||
---
|
||||
|
||||
## Exit Code Behavior (CRITICAL)
|
||||
|
||||
### Exit Code Reference Table
|
||||
|
||||
| Exit Code | stdout | stderr | Tool Execution | Claude Sees |
|
||||
|-----------|--------|--------|----------------|-------------|
|
||||
| 0 (UserPromptSubmit) | → Context | → User only | N/A | stdout content |
|
||||
| 0 (PreToolUse) | → User only | → User only | **Proceeds** | Nothing |
|
||||
| 2 (PreToolUse) | → User only | → **CLAUDE** | **BLOCKED** | stderr content |
|
||||
| Other | → User only | → User only | Blocked | Nothing |
|
||||
|
||||
### Why Exit Code 2 Matters
|
||||
|
||||
This is THE critical mechanism for enforcement:
|
||||
|
||||
1. **Only way** to send message to Claude from PreToolUse
|
||||
2. stderr content is "fed back to Claude automatically"
|
||||
3. Claude sees the block message and understands what to do
|
||||
4. Tool execution is prevented
|
||||
5. Critical for enforcement of guardrails
|
||||
|
||||
### Example Conversation Flow
|
||||
|
||||
```
|
||||
User: "Add a new user service with Prisma"
|
||||
|
||||
Claude: "I'll create the user service..."
|
||||
[Attempts to Edit form/src/services/user.ts]
|
||||
|
||||
PreToolUse Hook: [Exit code 2]
|
||||
stderr: "⚠️ BLOCKED - Use database-verification"
|
||||
|
||||
Claude sees error, responds:
|
||||
"I need to verify the database schema first."
|
||||
[Uses Skill tool: database-verification]
|
||||
[Verifies column names]
|
||||
[Retries Edit - now allowed (session tracking)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session State Management
|
||||
|
||||
### Purpose
|
||||
|
||||
Prevent repeated nagging in the same session - once Claude uses a skill, don't block again.
|
||||
|
||||
### State File Location
|
||||
|
||||
`.claude/hooks/state/skills-used-{session_id}.json`
|
||||
|
||||
### State File Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"skills_used": [
|
||||
"database-verification",
|
||||
"error-tracking"
|
||||
],
|
||||
"files_verified": []
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **First edit** of file with Prisma:
|
||||
- Hook blocks with exit code 2
|
||||
- Updates session state: adds "database-verification" to skills_used
|
||||
- Claude sees message, uses skill
|
||||
|
||||
2. **Second edit** (same session):
|
||||
- Hook checks session state
|
||||
- Finds "database-verification" in skills_used
|
||||
- Exits with code 0 (allow)
|
||||
- No message to Claude
|
||||
|
||||
3. **Different session**:
|
||||
- New session ID = new state file
|
||||
- Hook blocks again
|
||||
|
||||
### Limitation
|
||||
|
||||
The hook cannot detect when the skill is *actually* invoked - it just blocks once per session per skill. This means:
|
||||
|
||||
- If Claude doesn't use the skill but makes a different edit, it won't block again
|
||||
- Trust that Claude follows the instruction
|
||||
- Future enhancement: detect actual Skill tool usage
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Target Metrics
|
||||
|
||||
- **UserPromptSubmit**: < 100ms
|
||||
- **PreToolUse**: < 200ms
|
||||
|
||||
### Performance Bottlenecks
|
||||
|
||||
1. **Loading skill-rules.json** (every execution)
|
||||
- Future: Cache in memory
|
||||
- Future: Watch for changes, reload only when needed
|
||||
|
||||
2. **Reading file content** (PreToolUse)
|
||||
- Only when contentPatterns configured
|
||||
- Only if file exists
|
||||
- Can be slow for large files
|
||||
|
||||
3. **Glob matching** (PreToolUse)
|
||||
- Regex compilation for each pattern
|
||||
- Future: Compile once, cache
|
||||
|
||||
4. **Regex matching** (Both hooks)
|
||||
- Intent patterns (UserPromptSubmit)
|
||||
- Content patterns (PreToolUse)
|
||||
- Future: Lazy compile, cache compiled regexes
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
**Reduce patterns:**
|
||||
- Use more specific patterns (fewer to check)
|
||||
- Combine similar patterns where possible
|
||||
|
||||
**File path patterns:**
|
||||
- More specific = fewer files to check
|
||||
- Example: `form/src/services/**` better than `form/**`
|
||||
|
||||
**Content patterns:**
|
||||
- Only add when truly necessary
|
||||
- Simpler regex = faster matching
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
- [SKILL.md](SKILL.md) - Main skill guide
|
||||
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Debug hook issues
|
||||
- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Configuration reference
|
||||
@@ -1,152 +0,0 @@
|
||||
# Common Patterns Library
|
||||
|
||||
Ready-to-use regex and glob patterns for skill triggers. Copy and customize for your skills.
|
||||
|
||||
---
|
||||
|
||||
## Intent Patterns (Regex)
|
||||
|
||||
### Feature/Endpoint Creation
|
||||
```regex
|
||||
(add|create|implement|build).*?(feature|endpoint|route|service|controller)
|
||||
```
|
||||
|
||||
### Component Creation
|
||||
```regex
|
||||
(create|add|make|build).*?(component|UI|page|modal|dialog|form)
|
||||
```
|
||||
|
||||
### Database Work
|
||||
```regex
|
||||
(add|create|modify|update).*?(user|table|column|field|schema|migration)
|
||||
(database|prisma).*?(change|update|query)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```regex
|
||||
(fix|handle|catch|debug).*?(error|exception|bug)
|
||||
(add|implement).*?(try|catch|error.*?handling)
|
||||
```
|
||||
|
||||
### Explanation Requests
|
||||
```regex
|
||||
(how does|how do|explain|what is|describe|tell me about).*?
|
||||
```
|
||||
|
||||
### Workflow Operations
|
||||
```regex
|
||||
(create|add|modify|update).*?(workflow|step|branch|condition)
|
||||
(debug|troubleshoot|fix).*?workflow
|
||||
```
|
||||
|
||||
### Testing
|
||||
```regex
|
||||
(write|create|add).*?(test|spec|unit.*?test)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Path Patterns (Glob)
|
||||
|
||||
### Frontend
|
||||
```glob
|
||||
frontend/src/**/*.tsx # All React components
|
||||
frontend/src/**/*.ts # All TypeScript files
|
||||
frontend/src/components/** # Only components directory
|
||||
```
|
||||
|
||||
### Backend Services
|
||||
```glob
|
||||
form/src/**/*.ts # Form service
|
||||
email/src/**/*.ts # Email service
|
||||
users/src/**/*.ts # Users service
|
||||
projects/src/**/*.ts # Projects service
|
||||
```
|
||||
|
||||
### Database
|
||||
```glob
|
||||
**/schema.prisma # Prisma schema (anywhere)
|
||||
**/migrations/**/*.sql # Migration files
|
||||
database/src/**/*.ts # Database scripts
|
||||
```
|
||||
|
||||
### Workflows
|
||||
```glob
|
||||
form/src/workflow/**/*.ts # Workflow engine
|
||||
form/src/workflow-definitions/**/*.json # Workflow definitions
|
||||
```
|
||||
|
||||
### Test Exclusions
|
||||
```glob
|
||||
**/*.test.ts # TypeScript tests
|
||||
**/*.test.tsx # React component tests
|
||||
**/*.spec.ts # Spec files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Patterns (Regex)
|
||||
|
||||
### Prisma/Database
|
||||
```regex
|
||||
import.*[Pp]risma # Prisma imports
|
||||
PrismaService # PrismaService usage
|
||||
prisma\. # prisma.something
|
||||
\.findMany\( # Prisma query methods
|
||||
\.create\(
|
||||
\.update\(
|
||||
\.delete\(
|
||||
```
|
||||
|
||||
### Controllers/Routes
|
||||
```regex
|
||||
export class.*Controller # Controller classes
|
||||
router\. # Express router
|
||||
app\.(get|post|put|delete|patch) # Express app routes
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```regex
|
||||
try\s*\{ # Try blocks
|
||||
catch\s*\( # Catch blocks
|
||||
throw new # Throw statements
|
||||
```
|
||||
|
||||
### React/Components
|
||||
```regex
|
||||
export.*React\.FC # React functional components
|
||||
export default function.* # Default function exports
|
||||
useState|useEffect # React hooks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Usage Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"my-skill": {
|
||||
"promptTriggers": {
|
||||
"intentPatterns": [
|
||||
"(create|add|build).*?(component|UI|page)"
|
||||
]
|
||||
},
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"frontend/src/**/*.tsx"
|
||||
],
|
||||
"contentPatterns": [
|
||||
"export.*React\\.FC",
|
||||
"useState|useEffect"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
- [SKILL.md](SKILL.md) - Main skill guide
|
||||
- [TRIGGER_TYPES.md](TRIGGER_TYPES.md) - Detailed trigger documentation
|
||||
- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Complete schema
|
||||
@@ -1,426 +0,0 @@
|
||||
---
|
||||
name: skill-developer
|
||||
description: Create and manage Claude Code skills following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns, file paths, content patterns), enforcement levels (block, suggest, warn), hook mechanisms (UserPromptSubmit, PreToolUse), session tracking, and the 500-line rule.
|
||||
---
|
||||
|
||||
# Skill Developer Guide
|
||||
|
||||
## Purpose
|
||||
|
||||
Comprehensive guide for creating and managing skills in Claude Code with auto-activation system, following Anthropic's official best practices including the 500-line rule and progressive disclosure pattern.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Automatically activates when you mention:
|
||||
- Creating or adding skills
|
||||
- Modifying skill triggers or rules
|
||||
- Understanding how skill activation works
|
||||
- Debugging skill activation issues
|
||||
- Working with skill-rules.json
|
||||
- Hook system mechanics
|
||||
- Claude Code best practices
|
||||
- Progressive disclosure
|
||||
- YAML frontmatter
|
||||
- 500-line rule
|
||||
|
||||
---
|
||||
|
||||
## System Overview
|
||||
|
||||
### Two-Hook Architecture
|
||||
|
||||
**1. UserPromptSubmit Hook** (Proactive Suggestions)
|
||||
- **File**: `.claude/hooks/skill-activation-prompt.ts`
|
||||
- **Trigger**: BEFORE Claude sees user's prompt
|
||||
- **Purpose**: Suggest relevant skills based on keywords + intent patterns
|
||||
- **Method**: Injects formatted reminder as context (stdout → Claude's input)
|
||||
- **Use Cases**: Topic-based skills, implicit work detection
|
||||
|
||||
**2. Stop Hook - Error Handling Reminder** (Gentle Reminders)
|
||||
- **File**: `.claude/hooks/error-handling-reminder.ts`
|
||||
- **Trigger**: AFTER Claude finishes responding
|
||||
- **Purpose**: Gentle reminder to self-assess error handling in code written
|
||||
- **Method**: Analyzes edited files for risky patterns, displays reminder if needed
|
||||
- **Use Cases**: Error handling awareness without blocking friction
|
||||
|
||||
**Philosophy Change (2025-10-27):** We moved away from blocking PreToolUse for Sentry/error handling. Instead, use gentle post-response reminders that don't block workflow but maintain code quality awareness.
|
||||
|
||||
### Configuration File
|
||||
|
||||
**Location**: `.claude/skills/skill-rules.json`
|
||||
|
||||
Defines:
|
||||
- All skills and their trigger conditions
|
||||
- Enforcement levels (block, suggest, warn)
|
||||
- File path patterns (glob)
|
||||
- Content detection patterns (regex)
|
||||
- Skip conditions (session tracking, file markers, env vars)
|
||||
|
||||
---
|
||||
|
||||
## Skill Types
|
||||
|
||||
### 1. Guardrail Skills
|
||||
|
||||
**Purpose:** Enforce critical best practices that prevent errors
|
||||
|
||||
**Characteristics:**
|
||||
- Type: `"guardrail"`
|
||||
- Enforcement: `"block"`
|
||||
- Priority: `"critical"` or `"high"`
|
||||
- Block file edits until skill used
|
||||
- Prevent common mistakes (column names, critical errors)
|
||||
- Session-aware (don't repeat nag in same session)
|
||||
|
||||
**Examples:**
|
||||
- `database-verification` - Verify table/column names before Prisma queries
|
||||
- `frontend-dev-guidelines` - Enforce React/TypeScript patterns
|
||||
|
||||
**When to Use:**
|
||||
- Mistakes that cause runtime errors
|
||||
- Data integrity concerns
|
||||
- Critical compatibility issues
|
||||
|
||||
### 2. Domain Skills
|
||||
|
||||
**Purpose:** Provide comprehensive guidance for specific areas
|
||||
|
||||
**Characteristics:**
|
||||
- Type: `"domain"`
|
||||
- Enforcement: `"suggest"`
|
||||
- Priority: `"high"` or `"medium"`
|
||||
- Advisory, not mandatory
|
||||
- Topic or domain-specific
|
||||
- Comprehensive documentation
|
||||
|
||||
**Examples:**
|
||||
- `backend-dev-guidelines` - Node.js/Express/TypeScript patterns
|
||||
- `frontend-dev-guidelines` - React/TypeScript best practices
|
||||
- `error-tracking` - Sentry integration guidance
|
||||
|
||||
**When to Use:**
|
||||
- Complex systems requiring deep knowledge
|
||||
- Best practices documentation
|
||||
- Architectural patterns
|
||||
- How-to guides
|
||||
|
||||
---
|
||||
|
||||
## Quick Start: Creating a New Skill
|
||||
|
||||
### Step 1: Create Skill File
|
||||
|
||||
**Location:** `.claude/skills/{skill-name}/SKILL.md`
|
||||
|
||||
**Template:**
|
||||
```markdown
|
||||
---
|
||||
name: my-new-skill
|
||||
description: Brief description including keywords that trigger this skill. Mention topics, file types, and use cases. Be explicit about trigger terms.
|
||||
---
|
||||
|
||||
# My New Skill
|
||||
|
||||
## Purpose
|
||||
What this skill helps with
|
||||
|
||||
## When to Use
|
||||
Specific scenarios and conditions
|
||||
|
||||
## Key Information
|
||||
The actual guidance, documentation, patterns, examples
|
||||
```
|
||||
|
||||
**Best Practices:**
|
||||
- ✅ **Name**: Lowercase, hyphens, gerund form (verb + -ing) preferred
|
||||
- ✅ **Description**: Include ALL trigger keywords/phrases (max 1024 chars)
|
||||
- ✅ **Content**: Under 500 lines - use reference files for details
|
||||
- ✅ **Examples**: Real code examples
|
||||
- ✅ **Structure**: Clear headings, lists, code blocks
|
||||
|
||||
### Step 2: Add to skill-rules.json
|
||||
|
||||
See [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) for complete schema.
|
||||
|
||||
**Basic Template:**
|
||||
```json
|
||||
{
|
||||
"my-new-skill": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "medium",
|
||||
"promptTriggers": {
|
||||
"keywords": ["keyword1", "keyword2"],
|
||||
"intentPatterns": ["(create|add).*?something"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Test Triggers
|
||||
|
||||
**Test UserPromptSubmit:**
|
||||
```bash
|
||||
echo '{"session_id":"test","prompt":"your test prompt"}' | \
|
||||
npx tsx .claude/hooks/skill-activation-prompt.ts
|
||||
```
|
||||
|
||||
**Test PreToolUse:**
|
||||
```bash
|
||||
cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts
|
||||
{"session_id":"test","tool_name":"Edit","tool_input":{"file_path":"test.ts"}}
|
||||
EOF
|
||||
```
|
||||
|
||||
### Step 4: Refine Patterns
|
||||
|
||||
Based on testing:
|
||||
- Add missing keywords
|
||||
- Refine intent patterns to reduce false positives
|
||||
- Adjust file path patterns
|
||||
- Test content patterns against actual files
|
||||
|
||||
### Step 5: Follow Anthropic Best Practices
|
||||
|
||||
✅ Keep SKILL.md under 500 lines
|
||||
✅ Use progressive disclosure with reference files
|
||||
✅ Add table of contents to reference files > 100 lines
|
||||
✅ Write detailed description with trigger keywords
|
||||
✅ Test with 3+ real scenarios before documenting
|
||||
✅ Iterate based on actual usage
|
||||
|
||||
---
|
||||
|
||||
## Enforcement Levels
|
||||
|
||||
### BLOCK (Critical Guardrails)
|
||||
|
||||
- Physically prevents Edit/Write tool execution
|
||||
- Exit code 2 from hook, stderr → Claude
|
||||
- Claude sees message and must use skill to proceed
|
||||
- **Use For**: Critical mistakes, data integrity, security issues
|
||||
|
||||
**Example:** Database column name verification
|
||||
|
||||
### SUGGEST (Recommended)
|
||||
|
||||
- Reminder injected before Claude sees prompt
|
||||
- Claude is aware of relevant skills
|
||||
- Not enforced, just advisory
|
||||
- **Use For**: Domain guidance, best practices, how-to guides
|
||||
|
||||
**Example:** Frontend development guidelines
|
||||
|
||||
### WARN (Optional)
|
||||
|
||||
- Low priority suggestions
|
||||
- Advisory only, minimal enforcement
|
||||
- **Use For**: Nice-to-have suggestions, informational reminders
|
||||
|
||||
**Rarely used** - most skills are either BLOCK or SUGGEST.
|
||||
|
||||
---
|
||||
|
||||
## Skip Conditions & User Control
|
||||
|
||||
### 1. Session Tracking
|
||||
|
||||
**Purpose:** Don't nag repeatedly in same session
|
||||
|
||||
**How it works:**
|
||||
- First edit → Hook blocks, updates session state
|
||||
- Second edit (same session) → Hook allows
|
||||
- Different session → Blocks again
|
||||
|
||||
**State File:** `.claude/hooks/state/skills-used-{session_id}.json`
|
||||
|
||||
### 2. File Markers
|
||||
|
||||
**Purpose:** Permanent skip for verified files
|
||||
|
||||
**Marker:** `// @skip-validation`
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// @skip-validation
|
||||
import { PrismaService } from './prisma';
|
||||
// This file has been manually verified
|
||||
```
|
||||
|
||||
**NOTE:** Use sparingly - defeats the purpose if overused
|
||||
|
||||
### 3. Environment Variables
|
||||
|
||||
**Purpose:** Emergency disable, temporary override
|
||||
|
||||
**Global disable:**
|
||||
```bash
|
||||
export SKIP_SKILL_GUARDRAILS=true # Disables ALL PreToolUse blocks
|
||||
```
|
||||
|
||||
**Skill-specific:**
|
||||
```bash
|
||||
export SKIP_DB_VERIFICATION=true
|
||||
export SKIP_ERROR_REMINDER=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
When creating a new skill, verify:
|
||||
|
||||
- [ ] Skill file created in `.claude/skills/{name}/SKILL.md`
|
||||
- [ ] Proper frontmatter with name and description
|
||||
- [ ] Entry added to `skill-rules.json`
|
||||
- [ ] Keywords tested with real prompts
|
||||
- [ ] Intent patterns tested with variations
|
||||
- [ ] File path patterns tested with actual files
|
||||
- [ ] Content patterns tested against file contents
|
||||
- [ ] Block message is clear and actionable (if guardrail)
|
||||
- [ ] Skip conditions configured appropriately
|
||||
- [ ] Priority level matches importance
|
||||
- [ ] No false positives in testing
|
||||
- [ ] No false negatives in testing
|
||||
- [ ] Performance is acceptable (<100ms or <200ms)
|
||||
- [ ] JSON syntax validated: `jq . skill-rules.json`
|
||||
- [ ] **SKILL.md under 500 lines** ⭐
|
||||
- [ ] Reference files created if needed
|
||||
- [ ] Table of contents added to files > 100 lines
|
||||
|
||||
---
|
||||
|
||||
## Reference Files
|
||||
|
||||
For detailed information on specific topics, see:
|
||||
|
||||
### [TRIGGER_TYPES.md](TRIGGER_TYPES.md)
|
||||
Complete guide to all trigger types:
|
||||
- Keyword triggers (explicit topic matching)
|
||||
- Intent patterns (implicit action detection)
|
||||
- File path triggers (glob patterns)
|
||||
- Content patterns (regex in files)
|
||||
- Best practices and examples for each
|
||||
- Common pitfalls and testing strategies
|
||||
|
||||
### [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md)
|
||||
Complete skill-rules.json schema:
|
||||
- Full TypeScript interface definitions
|
||||
- Field-by-field explanations
|
||||
- Complete guardrail skill example
|
||||
- Complete domain skill example
|
||||
- Validation guide and common errors
|
||||
|
||||
### [HOOK_MECHANISMS.md](HOOK_MECHANISMS.md)
|
||||
Deep dive into hook internals:
|
||||
- UserPromptSubmit flow (detailed)
|
||||
- PreToolUse flow (detailed)
|
||||
- Exit code behavior table (CRITICAL)
|
||||
- Session state management
|
||||
- Performance considerations
|
||||
|
||||
### [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
|
||||
Comprehensive debugging guide:
|
||||
- Skill not triggering (UserPromptSubmit)
|
||||
- PreToolUse not blocking
|
||||
- False positives (too many triggers)
|
||||
- Hook not executing at all
|
||||
- Performance issues
|
||||
|
||||
### [PATTERNS_LIBRARY.md](PATTERNS_LIBRARY.md)
|
||||
Ready-to-use pattern collection:
|
||||
- Intent pattern library (regex)
|
||||
- File path pattern library (glob)
|
||||
- Content pattern library (regex)
|
||||
- Organized by use case
|
||||
- Copy-paste ready
|
||||
|
||||
### [ADVANCED.md](ADVANCED.md)
|
||||
Future enhancements and ideas:
|
||||
- Dynamic rule updates
|
||||
- Skill dependencies
|
||||
- Conditional enforcement
|
||||
- Skill analytics
|
||||
- Skill versioning
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Summary
|
||||
|
||||
### Create New Skill (5 Steps)
|
||||
|
||||
1. Create `.claude/skills/{name}/SKILL.md` with frontmatter
|
||||
2. Add entry to `.claude/skills/skill-rules.json`
|
||||
3. Test with `npx tsx` commands
|
||||
4. Refine patterns based on testing
|
||||
5. Keep SKILL.md under 500 lines
|
||||
|
||||
### Trigger Types
|
||||
|
||||
- **Keywords**: Explicit topic mentions
|
||||
- **Intent**: Implicit action detection
|
||||
- **File Paths**: Location-based activation
|
||||
- **Content**: Technology-specific detection
|
||||
|
||||
See [TRIGGER_TYPES.md](TRIGGER_TYPES.md) for complete details.
|
||||
|
||||
### Enforcement
|
||||
|
||||
- **BLOCK**: Exit code 2, critical only
|
||||
- **SUGGEST**: Inject context, most common
|
||||
- **WARN**: Advisory, rarely used
|
||||
|
||||
### Skip Conditions
|
||||
|
||||
- **Session tracking**: Automatic (prevents repeated nags)
|
||||
- **File markers**: `// @skip-validation` (permanent skip)
|
||||
- **Env vars**: `SKIP_SKILL_GUARDRAILS` (emergency disable)
|
||||
|
||||
### Anthropic Best Practices
|
||||
|
||||
✅ **500-line rule**: Keep SKILL.md under 500 lines
|
||||
✅ **Progressive disclosure**: Use reference files for details
|
||||
✅ **Table of contents**: Add to reference files > 100 lines
|
||||
✅ **One level deep**: Don't nest references deeply
|
||||
✅ **Rich descriptions**: Include all trigger keywords (max 1024 chars)
|
||||
✅ **Test first**: Build 3+ evaluations before extensive documentation
|
||||
✅ **Gerund naming**: Prefer verb + -ing (e.g., "processing-pdfs")
|
||||
|
||||
### Troubleshoot
|
||||
|
||||
Test hooks manually:
|
||||
```bash
|
||||
# UserPromptSubmit
|
||||
echo '{"prompt":"test"}' | npx tsx .claude/hooks/skill-activation-prompt.ts
|
||||
|
||||
# PreToolUse
|
||||
cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts
|
||||
{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}}
|
||||
EOF
|
||||
```
|
||||
|
||||
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for complete debugging guide.
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
**Configuration:**
|
||||
- `.claude/skills/skill-rules.json` - Master configuration
|
||||
- `.claude/hooks/state/` - Session tracking
|
||||
- `.claude/settings.json` - Hook registration
|
||||
|
||||
**Hooks:**
|
||||
- `.claude/hooks/skill-activation-prompt.ts` - UserPromptSubmit
|
||||
- `.claude/hooks/error-handling-reminder.ts` - Stop event (gentle reminders)
|
||||
|
||||
**All Skills:**
|
||||
- `.claude/skills/*/SKILL.md` - Skill content files
|
||||
|
||||
---
|
||||
|
||||
**Skill Status**: COMPLETE - Restructured following Anthropic best practices ✅
|
||||
**Line Count**: < 500 (following 500-line rule) ✅
|
||||
**Progressive Disclosure**: Reference files for detailed information ✅
|
||||
|
||||
**Next**: Create more skills, refine patterns based on usage
|
||||
@@ -1,315 +0,0 @@
|
||||
# skill-rules.json - Complete Reference
|
||||
|
||||
Complete schema and configuration reference for `.claude/skills/skill-rules.json`.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [File Location](#file-location)
|
||||
- [Complete TypeScript Schema](#complete-typescript-schema)
|
||||
- [Field Guide](#field-guide)
|
||||
- [Example: Guardrail Skill](#example-guardrail-skill)
|
||||
- [Example: Domain Skill](#example-domain-skill)
|
||||
- [Validation](#validation)
|
||||
|
||||
---
|
||||
|
||||
## File Location
|
||||
|
||||
**Path:** `.claude/skills/skill-rules.json`
|
||||
|
||||
This JSON file defines all skills and their trigger conditions for the auto-activation system.
|
||||
|
||||
---
|
||||
|
||||
## Complete TypeScript Schema
|
||||
|
||||
```typescript
|
||||
interface SkillRules {
|
||||
version: string;
|
||||
skills: Record<string, SkillRule>;
|
||||
}
|
||||
|
||||
interface SkillRule {
|
||||
type: 'guardrail' | 'domain';
|
||||
enforcement: 'block' | 'suggest' | 'warn';
|
||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||
|
||||
promptTriggers?: {
|
||||
keywords?: string[];
|
||||
intentPatterns?: string[]; // Regex strings
|
||||
};
|
||||
|
||||
fileTriggers?: {
|
||||
pathPatterns: string[]; // Glob patterns
|
||||
pathExclusions?: string[]; // Glob patterns
|
||||
contentPatterns?: string[]; // Regex strings
|
||||
createOnly?: boolean; // Only trigger on file creation
|
||||
};
|
||||
|
||||
blockMessage?: string; // For guardrails, {file_path} placeholder
|
||||
|
||||
skipConditions?: {
|
||||
sessionSkillUsed?: boolean; // Skip if used in session
|
||||
fileMarkers?: string[]; // e.g., ["@skip-validation"]
|
||||
envOverride?: string; // e.g., "SKIP_DB_VERIFICATION"
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field Guide
|
||||
|
||||
### Top Level
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `version` | string | Yes | Schema version (currently "1.0") |
|
||||
| `skills` | object | Yes | Map of skill name → SkillRule |
|
||||
|
||||
### SkillRule Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `type` | string | Yes | "guardrail" (enforced) or "domain" (advisory) |
|
||||
| `enforcement` | string | Yes | "block" (PreToolUse), "suggest" (UserPromptSubmit), or "warn" |
|
||||
| `priority` | string | Yes | "critical", "high", "medium", or "low" |
|
||||
| `promptTriggers` | object | Optional | Triggers for UserPromptSubmit hook |
|
||||
| `fileTriggers` | object | Optional | Triggers for PreToolUse hook |
|
||||
| `blockMessage` | string | Optional* | Required if enforcement="block". Use `{file_path}` placeholder |
|
||||
| `skipConditions` | object | Optional | Escape hatches and session tracking |
|
||||
|
||||
*Required for guardrails
|
||||
|
||||
### promptTriggers Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `keywords` | string[] | Optional | Exact substring matches (case-insensitive) |
|
||||
| `intentPatterns` | string[] | Optional | Regex patterns for intent detection |
|
||||
|
||||
### fileTriggers Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `pathPatterns` | string[] | Yes* | Glob patterns for file paths |
|
||||
| `pathExclusions` | string[] | Optional | Glob patterns to exclude (e.g., test files) |
|
||||
| `contentPatterns` | string[] | Optional | Regex patterns to match file content |
|
||||
| `createOnly` | boolean | Optional | Only trigger when creating new files |
|
||||
|
||||
*Required if fileTriggers is present
|
||||
|
||||
### skipConditions Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `sessionSkillUsed` | boolean | Optional | Skip if skill already used this session |
|
||||
| `fileMarkers` | string[] | Optional | Skip if file contains comment marker |
|
||||
| `envOverride` | string | Optional | Environment variable name to disable skill |
|
||||
|
||||
---
|
||||
|
||||
## Example: Guardrail Skill
|
||||
|
||||
Complete example of a blocking guardrail skill with all features:
|
||||
|
||||
```json
|
||||
{
|
||||
"database-verification": {
|
||||
"type": "guardrail",
|
||||
"enforcement": "block",
|
||||
"priority": "critical",
|
||||
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"prisma",
|
||||
"database",
|
||||
"table",
|
||||
"column",
|
||||
"schema",
|
||||
"query",
|
||||
"migration"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(add|create|implement).*?(user|login|auth|tracking|feature)",
|
||||
"(modify|update|change).*?(table|column|schema|field)",
|
||||
"database.*?(change|update|modify|migration)"
|
||||
]
|
||||
},
|
||||
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"**/schema.prisma",
|
||||
"**/migrations/**/*.sql",
|
||||
"database/src/**/*.ts",
|
||||
"form/src/**/*.ts",
|
||||
"email/src/**/*.ts",
|
||||
"users/src/**/*.ts",
|
||||
"projects/src/**/*.ts",
|
||||
"utilities/src/**/*.ts"
|
||||
],
|
||||
"pathExclusions": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"contentPatterns": [
|
||||
"import.*[Pp]risma",
|
||||
"PrismaService",
|
||||
"prisma\\.",
|
||||
"\\.findMany\\(",
|
||||
"\\.findUnique\\(",
|
||||
"\\.findFirst\\(",
|
||||
"\\.create\\(",
|
||||
"\\.createMany\\(",
|
||||
"\\.update\\(",
|
||||
"\\.updateMany\\(",
|
||||
"\\.upsert\\(",
|
||||
"\\.delete\\(",
|
||||
"\\.deleteMany\\("
|
||||
]
|
||||
},
|
||||
|
||||
"blockMessage": "⚠️ BLOCKED - Database Operation Detected\n\n📋 REQUIRED ACTION:\n1. Use Skill tool: 'database-verification'\n2. Verify ALL table and column names against schema\n3. Check database structure with DESCRIBE commands\n4. Then retry this edit\n\nReason: Prevent column name errors in Prisma queries\nFile: {file_path}\n\n💡 TIP: Add '// @skip-validation' comment to skip future checks",
|
||||
|
||||
"skipConditions": {
|
||||
"sessionSkillUsed": true,
|
||||
"fileMarkers": [
|
||||
"@skip-validation"
|
||||
],
|
||||
"envOverride": "SKIP_DB_VERIFICATION"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Points for Guardrails
|
||||
|
||||
1. **type**: Must be "guardrail"
|
||||
2. **enforcement**: Must be "block"
|
||||
3. **priority**: Usually "critical" or "high"
|
||||
4. **blockMessage**: Required, clear actionable steps
|
||||
5. **skipConditions**: Session tracking prevents repeated nagging
|
||||
6. **fileTriggers**: Usually has both path and content patterns
|
||||
7. **contentPatterns**: Catch actual usage of technology
|
||||
|
||||
---
|
||||
|
||||
## Example: Domain Skill
|
||||
|
||||
Complete example of a suggestion-based domain skill:
|
||||
|
||||
```json
|
||||
{
|
||||
"project-catalog-developer": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"layout",
|
||||
"layout system",
|
||||
"grid",
|
||||
"grid layout",
|
||||
"toolbar",
|
||||
"column",
|
||||
"cell editor",
|
||||
"cell renderer",
|
||||
"submission",
|
||||
"submissions",
|
||||
"blog dashboard",
|
||||
"datagrid",
|
||||
"data grid",
|
||||
"CustomToolbar",
|
||||
"GridLayoutDialog",
|
||||
"useGridLayout",
|
||||
"auto-save",
|
||||
"column order",
|
||||
"column width",
|
||||
"filter",
|
||||
"sort"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(how does|how do|explain|what is|describe).*?(layout|grid|toolbar|column|submission|catalog)",
|
||||
"(add|create|modify|change).*?(toolbar|column|cell|editor|renderer)",
|
||||
"blog dashboard.*?"
|
||||
]
|
||||
},
|
||||
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"frontend/src/features/submissions/**/*.tsx",
|
||||
"frontend/src/features/submissions/**/*.ts"
|
||||
],
|
||||
"pathExclusions": [
|
||||
"**/*.test.tsx",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Points for Domain Skills
|
||||
|
||||
1. **type**: Must be "domain"
|
||||
2. **enforcement**: Usually "suggest"
|
||||
3. **priority**: "high" or "medium"
|
||||
4. **blockMessage**: Not needed (doesn't block)
|
||||
5. **skipConditions**: Optional (less critical)
|
||||
6. **promptTriggers**: Usually has extensive keywords
|
||||
7. **fileTriggers**: May have only path patterns (content less important)
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
### Check JSON Syntax
|
||||
|
||||
```bash
|
||||
cat .claude/skills/skill-rules.json | jq .
|
||||
```
|
||||
|
||||
If valid, jq will pretty-print the JSON. If invalid, it will show the error.
|
||||
|
||||
### Common JSON Errors
|
||||
|
||||
**Trailing comma:**
|
||||
```json
|
||||
{
|
||||
"keywords": ["one", "two",] // ❌ Trailing comma
|
||||
}
|
||||
```
|
||||
|
||||
**Missing quotes:**
|
||||
```json
|
||||
{
|
||||
type: "guardrail" // ❌ Missing quotes on key
|
||||
}
|
||||
```
|
||||
|
||||
**Single quotes (invalid JSON):**
|
||||
```json
|
||||
{
|
||||
'type': 'guardrail' // ❌ Must use double quotes
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Checklist
|
||||
|
||||
- [ ] JSON syntax valid (use `jq`)
|
||||
- [ ] All skill names match SKILL.md filenames
|
||||
- [ ] Guardrails have `blockMessage`
|
||||
- [ ] Block messages use `{file_path}` placeholder
|
||||
- [ ] Intent patterns are valid regex (test on regex101.com)
|
||||
- [ ] File path patterns use correct glob syntax
|
||||
- [ ] Content patterns escape special characters
|
||||
- [ ] Priority matches enforcement level
|
||||
- [ ] No duplicate skill names
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
- [SKILL.md](SKILL.md) - Main skill guide
|
||||
- [TRIGGER_TYPES.md](TRIGGER_TYPES.md) - Complete trigger documentation
|
||||
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Debugging configuration issues
|
||||
@@ -1,305 +0,0 @@
|
||||
# Trigger Types - Complete Guide
|
||||
|
||||
Complete reference for configuring skill triggers in Claude Code's skill auto-activation system.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Keyword Triggers (Explicit)](#keyword-triggers-explicit)
|
||||
- [Intent Pattern Triggers (Implicit)](#intent-pattern-triggers-implicit)
|
||||
- [File Path Triggers](#file-path-triggers)
|
||||
- [Content Pattern Triggers](#content-pattern-triggers)
|
||||
- [Best Practices Summary](#best-practices-summary)
|
||||
|
||||
---
|
||||
|
||||
## Keyword Triggers (Explicit)
|
||||
|
||||
### How It Works
|
||||
|
||||
Case-insensitive substring matching in user's prompt.
|
||||
|
||||
### Use For
|
||||
|
||||
Topic-based activation where user explicitly mentions the subject.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
"promptTriggers": {
|
||||
"keywords": ["layout", "grid", "toolbar", "submission"]
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
- User prompt: "how does the **layout** system work?"
|
||||
- Matches: "layout" keyword
|
||||
- Activates: `project-catalog-developer`
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Use specific, unambiguous terms
|
||||
- Include common variations ("layout", "layout system", "grid layout")
|
||||
- Avoid overly generic words ("system", "work", "create")
|
||||
- Test with real prompts
|
||||
|
||||
---
|
||||
|
||||
## Intent Pattern Triggers (Implicit)
|
||||
|
||||
### How It Works
|
||||
|
||||
Regex pattern matching to detect user's intent even when they don't mention the topic explicitly.
|
||||
|
||||
### Use For
|
||||
|
||||
Action-based activation where user describes what they want to do rather than the specific topic.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
"promptTriggers": {
|
||||
"intentPatterns": [
|
||||
"(create|add|implement).*?(feature|endpoint)",
|
||||
"(how does|explain).*?(layout|workflow)"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
**Database Work:**
|
||||
- User prompt: "add user tracking feature"
|
||||
- Matches: `(add).*?(feature)`
|
||||
- Activates: `database-verification`, `error-tracking`
|
||||
|
||||
**Component Creation:**
|
||||
- User prompt: "create a dashboard widget"
|
||||
- Matches: `(create).*?(component)` (if component in pattern)
|
||||
- Activates: `frontend-dev-guidelines`
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Capture common action verbs: `(create|add|modify|build|implement)`
|
||||
- Include domain-specific nouns: `(feature|endpoint|component|workflow)`
|
||||
- Use non-greedy matching: `.*?` instead of `.*`
|
||||
- Test patterns thoroughly with regex tester (https://regex101.com/)
|
||||
- Don't make patterns too broad (causes false positives)
|
||||
- Don't make patterns too specific (causes false negatives)
|
||||
|
||||
### Common Pattern Examples
|
||||
|
||||
```regex
|
||||
# Database Work
|
||||
(add|create|implement).*?(user|login|auth|feature)
|
||||
|
||||
# Explanations
|
||||
(how does|explain|what is|describe).*?
|
||||
|
||||
# Frontend Work
|
||||
(create|add|make|build).*?(component|UI|page|modal|dialog)
|
||||
|
||||
# Error Handling
|
||||
(fix|handle|catch|debug).*?(error|exception|bug)
|
||||
|
||||
# Workflow Operations
|
||||
(create|add|modify).*?(workflow|step|branch|condition)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Path Triggers
|
||||
|
||||
### How It Works
|
||||
|
||||
Glob pattern matching against the file path being edited.
|
||||
|
||||
### Use For
|
||||
|
||||
Domain/area-specific activation based on file location in the project.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"frontend/src/**/*.tsx",
|
||||
"form/src/**/*.ts"
|
||||
],
|
||||
"pathExclusions": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Glob Pattern Syntax
|
||||
|
||||
- `**` = Any number of directories (including zero)
|
||||
- `*` = Any characters within a directory name
|
||||
- Examples:
|
||||
- `frontend/src/**/*.tsx` = All .tsx files in frontend/src and subdirs
|
||||
- `**/schema.prisma` = schema.prisma anywhere in project
|
||||
- `form/src/**/*.ts` = All .ts files in form/src subdirs
|
||||
|
||||
### Example
|
||||
|
||||
- File being edited: `frontend/src/components/Dashboard.tsx`
|
||||
- Matches: `frontend/src/**/*.tsx`
|
||||
- Activates: `frontend-dev-guidelines`
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Be specific to avoid false positives
|
||||
- Use exclusions for test files: `**/*.test.ts`
|
||||
- Consider subdirectory structure
|
||||
- Test patterns with actual file paths
|
||||
- Use narrower patterns when possible: `form/src/services/**` not `form/**`
|
||||
|
||||
### Common Path Patterns
|
||||
|
||||
```glob
|
||||
# Frontend
|
||||
frontend/src/**/*.tsx # All React components
|
||||
frontend/src/**/*.ts # All TypeScript files
|
||||
frontend/src/components/** # Only components directory
|
||||
|
||||
# Backend Services
|
||||
form/src/**/*.ts # Form service
|
||||
email/src/**/*.ts # Email service
|
||||
users/src/**/*.ts # Users service
|
||||
|
||||
# Database
|
||||
**/schema.prisma # Prisma schema (anywhere)
|
||||
**/migrations/**/*.sql # Migration files
|
||||
database/src/**/*.ts # Database scripts
|
||||
|
||||
# Workflows
|
||||
form/src/workflow/**/*.ts # Workflow engine
|
||||
form/src/workflow-definitions/**/*.json # Workflow definitions
|
||||
|
||||
# Test Exclusions
|
||||
**/*.test.ts # TypeScript tests
|
||||
**/*.test.tsx # React component tests
|
||||
**/*.spec.ts # Spec files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Pattern Triggers
|
||||
|
||||
### How It Works
|
||||
|
||||
Regex pattern matching against the file's actual content (what's inside the file).
|
||||
|
||||
### Use For
|
||||
|
||||
Technology-specific activation based on what the code imports or uses (Prisma, controllers, specific libraries).
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
"fileTriggers": {
|
||||
"contentPatterns": [
|
||||
"import.*[Pp]risma",
|
||||
"PrismaService",
|
||||
"\\.findMany\\(",
|
||||
"\\.create\\("
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
**Prisma Detection:**
|
||||
- File contains: `import { PrismaService } from '@project/database'`
|
||||
- Matches: `import.*[Pp]risma`
|
||||
- Activates: `database-verification`
|
||||
|
||||
**Controller Detection:**
|
||||
- File contains: `export class UserController {`
|
||||
- Matches: `export class.*Controller`
|
||||
- Activates: `error-tracking`
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Match imports: `import.*[Pp]risma` (case-insensitive with [Pp])
|
||||
- Escape special regex chars: `\\.findMany\\(` not `.findMany(`
|
||||
- Patterns use case-insensitive flag
|
||||
- Test against real file content
|
||||
- Make patterns specific enough to avoid false matches
|
||||
|
||||
### Common Content Patterns
|
||||
|
||||
```regex
|
||||
# Prisma/Database
|
||||
import.*[Pp]risma # Prisma imports
|
||||
PrismaService # PrismaService usage
|
||||
prisma\. # prisma.something
|
||||
\.findMany\( # Prisma query methods
|
||||
\.create\(
|
||||
\.update\(
|
||||
\.delete\(
|
||||
|
||||
# Controllers/Routes
|
||||
export class.*Controller # Controller classes
|
||||
router\. # Express router
|
||||
app\.(get|post|put|delete|patch) # Express app routes
|
||||
|
||||
# Error Handling
|
||||
try\s*\{ # Try blocks
|
||||
catch\s*\( # Catch blocks
|
||||
throw new # Throw statements
|
||||
|
||||
# React/Components
|
||||
export.*React\.FC # React functional components
|
||||
export default function.* # Default function exports
|
||||
useState|useEffect # React hooks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
### DO:
|
||||
✅ Use specific, unambiguous keywords
|
||||
✅ Test all patterns with real examples
|
||||
✅ Include common variations
|
||||
✅ Use non-greedy regex: `.*?`
|
||||
✅ Escape special characters in content patterns
|
||||
✅ Add exclusions for test files
|
||||
✅ Make file path patterns narrow and specific
|
||||
|
||||
### DON'T:
|
||||
❌ Use overly generic keywords ("system", "work")
|
||||
❌ Make intent patterns too broad (false positives)
|
||||
❌ Make patterns too specific (false negatives)
|
||||
❌ Forget to test with regex tester (https://regex101.com/)
|
||||
❌ Use greedy regex: `.*` instead of `.*?`
|
||||
❌ Match too broadly in file paths
|
||||
|
||||
### Testing Your Triggers
|
||||
|
||||
**Test keyword/intent triggers:**
|
||||
```bash
|
||||
echo '{"session_id":"test","prompt":"your test prompt"}' | \
|
||||
npx tsx .claude/hooks/skill-activation-prompt.ts
|
||||
```
|
||||
|
||||
**Test file path/content triggers:**
|
||||
```bash
|
||||
cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts
|
||||
{
|
||||
"session_id": "test",
|
||||
"tool_name": "Edit",
|
||||
"tool_input": {"file_path": "/path/to/test/file.ts"}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
- [SKILL.md](SKILL.md) - Main skill guide
|
||||
- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Complete skill-rules.json schema
|
||||
- [PATTERNS_LIBRARY.md](PATTERNS_LIBRARY.md) - Ready-to-use pattern library
|
||||
@@ -1,514 +0,0 @@
|
||||
# Troubleshooting - Skill Activation Issues
|
||||
|
||||
Complete debugging guide for skill activation problems.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Skill Not Triggering](#skill-not-triggering)
|
||||
- [UserPromptSubmit Not Suggesting](#userpromptsubmit-not-suggesting)
|
||||
- [PreToolUse Not Blocking](#pretooluse-not-blocking)
|
||||
- [False Positives](#false-positives)
|
||||
- [Hook Not Executing](#hook-not-executing)
|
||||
- [Performance Issues](#performance-issues)
|
||||
|
||||
---
|
||||
|
||||
## Skill Not Triggering
|
||||
|
||||
### UserPromptSubmit Not Suggesting
|
||||
|
||||
**Symptoms:** Ask a question, but no skill suggestion appears in output.
|
||||
|
||||
**Common Causes:**
|
||||
|
||||
#### 1. Keywords Don't Match
|
||||
|
||||
**Check:**
|
||||
- Look at `promptTriggers.keywords` in skill-rules.json
|
||||
- Are the keywords actually in your prompt?
|
||||
- Remember: case-insensitive substring matching
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"keywords": ["layout", "grid"]
|
||||
```
|
||||
- "how does the layout work?" → ✅ Matches "layout"
|
||||
- "how does the grid system work?" → ✅ Matches "grid"
|
||||
- "how do layouts work?" → ✅ Matches "layout"
|
||||
- "how does it work?" → ❌ No match
|
||||
|
||||
**Fix:** Add more keyword variations to skill-rules.json
|
||||
|
||||
#### 2. Intent Patterns Too Specific
|
||||
|
||||
**Check:**
|
||||
- Look at `promptTriggers.intentPatterns`
|
||||
- Test regex at https://regex101.com/
|
||||
- May need broader patterns
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"intentPatterns": [
|
||||
"(create|add).*?(database.*?table)" // Too specific
|
||||
]
|
||||
```
|
||||
- "create a database table" → ✅ Matches
|
||||
- "add new table" → ❌ Doesn't match (missing "database")
|
||||
|
||||
**Fix:** Broaden the pattern:
|
||||
```json
|
||||
"intentPatterns": [
|
||||
"(create|add).*?(table|database)" // Better
|
||||
]
|
||||
```
|
||||
|
||||
#### 3. Typo in Skill Name
|
||||
|
||||
**Check:**
|
||||
- Skill name in SKILL.md frontmatter
|
||||
- Skill name in skill-rules.json
|
||||
- Must match exactly
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
# SKILL.md
|
||||
name: project-catalog-developer
|
||||
```
|
||||
```json
|
||||
// skill-rules.json
|
||||
"project-catalogue-developer": { // ❌ Typo: catalogue vs catalog
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:** Make names match exactly
|
||||
|
||||
#### 4. JSON Syntax Error
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
cat .claude/skills/skill-rules.json | jq .
|
||||
```
|
||||
|
||||
If invalid JSON, jq will show the error.
|
||||
|
||||
**Common errors:**
|
||||
- Trailing commas
|
||||
- Missing quotes
|
||||
- Single quotes instead of double
|
||||
- Unescaped characters in strings
|
||||
|
||||
**Fix:** Correct JSON syntax, validate with jq
|
||||
|
||||
#### Debug Command
|
||||
|
||||
Test the hook manually:
|
||||
|
||||
```bash
|
||||
echo '{"session_id":"debug","prompt":"your test prompt here"}' | \
|
||||
npx tsx .claude/hooks/skill-activation-prompt.ts
|
||||
```
|
||||
|
||||
Expected: Your skill should appear in the output.
|
||||
|
||||
---
|
||||
|
||||
### PreToolUse Not Blocking
|
||||
|
||||
**Symptoms:** Edit a file that should trigger a guardrail, but no block occurs.
|
||||
|
||||
**Common Causes:**
|
||||
|
||||
#### 1. File Path Doesn't Match Patterns
|
||||
|
||||
**Check:**
|
||||
- File path being edited
|
||||
- `fileTriggers.pathPatterns` in skill-rules.json
|
||||
- Glob pattern syntax
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"pathPatterns": [
|
||||
"frontend/src/**/*.tsx"
|
||||
]
|
||||
```
|
||||
- Editing: `frontend/src/components/Dashboard.tsx` → ✅ Matches
|
||||
- Editing: `frontend/tests/Dashboard.test.tsx` → ✅ Matches (add exclusion!)
|
||||
- Editing: `backend/src/app.ts` → ❌ Doesn't match
|
||||
|
||||
**Fix:** Adjust glob patterns or add the missing path
|
||||
|
||||
#### 2. Excluded by pathExclusions
|
||||
|
||||
**Check:**
|
||||
- Are you editing a test file?
|
||||
- Look at `fileTriggers.pathExclusions`
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"pathExclusions": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
```
|
||||
- Editing: `services/user.test.ts` → ❌ Excluded
|
||||
- Editing: `services/user.ts` → ✅ Not excluded
|
||||
|
||||
**Fix:** If test exclusion too broad, narrow it or remove
|
||||
|
||||
#### 3. Content Pattern Not Found
|
||||
|
||||
**Check:**
|
||||
- Does the file actually contain the pattern?
|
||||
- Look at `fileTriggers.contentPatterns`
|
||||
- Is the regex correct?
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"contentPatterns": [
|
||||
"import.*[Pp]risma"
|
||||
]
|
||||
```
|
||||
- File has: `import { PrismaService } from './prisma'` → ✅ Matches
|
||||
- File has: `import { Database } from './db'` → ❌ Doesn't match
|
||||
|
||||
**Debug:**
|
||||
```bash
|
||||
# Check if pattern exists in file
|
||||
grep -i "prisma" path/to/file.ts
|
||||
```
|
||||
|
||||
**Fix:** Adjust content patterns or add missing imports
|
||||
|
||||
#### 4. Session Already Used Skill
|
||||
|
||||
**Check session state:**
|
||||
```bash
|
||||
ls .claude/hooks/state/
|
||||
cat .claude/hooks/state/skills-used-{session-id}.json
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"skills_used": ["database-verification"],
|
||||
"files_verified": []
|
||||
}
|
||||
```
|
||||
|
||||
If the skill is in `skills_used`, it won't block again in this session.
|
||||
|
||||
**Fix:** Delete the state file to reset:
|
||||
```bash
|
||||
rm .claude/hooks/state/skills-used-{session-id}.json
|
||||
```
|
||||
|
||||
#### 5. File Marker Present
|
||||
|
||||
**Check file for skip marker:**
|
||||
```bash
|
||||
grep "@skip-validation" path/to/file.ts
|
||||
```
|
||||
|
||||
If found, the file is permanently skipped.
|
||||
|
||||
**Fix:** Remove the marker if verification is needed again
|
||||
|
||||
#### 6. Environment Variable Override
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
echo $SKIP_DB_VERIFICATION
|
||||
echo $SKIP_SKILL_GUARDRAILS
|
||||
```
|
||||
|
||||
If set, the skill is disabled.
|
||||
|
||||
**Fix:** Unset the environment variable:
|
||||
```bash
|
||||
unset SKIP_DB_VERIFICATION
|
||||
```
|
||||
|
||||
#### Debug Command
|
||||
|
||||
Test the hook manually:
|
||||
|
||||
```bash
|
||||
cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts 2>&1
|
||||
{
|
||||
"session_id": "debug",
|
||||
"tool_name": "Edit",
|
||||
"tool_input": {"file_path": "/root/git/your-project/form/src/services/user.ts"}
|
||||
}
|
||||
EOF
|
||||
echo "Exit code: $?"
|
||||
```
|
||||
|
||||
Expected:
|
||||
- Exit code 2 + stderr message if should block
|
||||
- Exit code 0 + no output if should allow
|
||||
|
||||
---
|
||||
|
||||
## False Positives
|
||||
|
||||
**Symptoms:** Skill triggers when it shouldn't.
|
||||
|
||||
**Common Causes & Solutions:**
|
||||
|
||||
### 1. Keywords Too Generic
|
||||
|
||||
**Problem:**
|
||||
```json
|
||||
"keywords": ["user", "system", "create"] // Too broad
|
||||
```
|
||||
- Triggers on: "user manual", "file system", "create directory"
|
||||
|
||||
**Solution:** Make keywords more specific
|
||||
```json
|
||||
"keywords": [
|
||||
"user authentication",
|
||||
"user tracking",
|
||||
"create feature"
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Intent Patterns Too Broad
|
||||
|
||||
**Problem:**
|
||||
```json
|
||||
"intentPatterns": [
|
||||
"(create)" // Matches everything with "create"
|
||||
]
|
||||
```
|
||||
- Triggers on: "create file", "create folder", "create account"
|
||||
|
||||
**Solution:** Add context to patterns
|
||||
```json
|
||||
"intentPatterns": [
|
||||
"(create|add).*?(database|table|feature)" // More specific
|
||||
]
|
||||
```
|
||||
|
||||
**Advanced:** Use negative lookaheads to exclude
|
||||
```regex
|
||||
(create)(?!.*test).*?(feature) // Don't match if "test" appears
|
||||
```
|
||||
|
||||
### 3. File Paths Too Generic
|
||||
|
||||
**Problem:**
|
||||
```json
|
||||
"pathPatterns": [
|
||||
"form/**" // Matches everything in form/
|
||||
]
|
||||
```
|
||||
- Triggers on: test files, config files, everything
|
||||
|
||||
**Solution:** Use narrower patterns
|
||||
```json
|
||||
"pathPatterns": [
|
||||
"form/src/services/**/*.ts", // Only service files
|
||||
"form/src/controllers/**/*.ts"
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Content Patterns Catching Unrelated Code
|
||||
|
||||
**Problem:**
|
||||
```json
|
||||
"contentPatterns": [
|
||||
"Prisma" // Matches in comments, strings, etc.
|
||||
]
|
||||
```
|
||||
- Triggers on: `// Don't use Prisma here`
|
||||
- Triggers on: `const note = "Prisma is cool"`
|
||||
|
||||
**Solution:** Make patterns more specific
|
||||
```json
|
||||
"contentPatterns": [
|
||||
"import.*[Pp]risma", // Only imports
|
||||
"PrismaService\\.", // Only actual usage
|
||||
"prisma\\.(findMany|create)" // Specific methods
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Adjust Enforcement Level
|
||||
|
||||
**Last resort:** If false positives are frequent:
|
||||
|
||||
```json
|
||||
{
|
||||
"enforcement": "block" // Change to "suggest"
|
||||
}
|
||||
```
|
||||
|
||||
This makes it advisory instead of blocking.
|
||||
|
||||
---
|
||||
|
||||
## Hook Not Executing
|
||||
|
||||
**Symptoms:** Hook doesn't run at all - no suggestion, no block.
|
||||
|
||||
**Common Causes:**
|
||||
|
||||
### 1. Hook Not Registered
|
||||
|
||||
**Check `.claude/settings.json`:**
|
||||
```bash
|
||||
cat .claude/settings.json | jq '.hooks.UserPromptSubmit'
|
||||
cat .claude/settings.json | jq '.hooks.PreToolUse'
|
||||
```
|
||||
|
||||
Expected: Hook entries present
|
||||
|
||||
**Fix:** Add missing hook registration:
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Bash Wrapper Not Executable
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
ls -l .claude/hooks/*.sh
|
||||
```
|
||||
|
||||
Expected: `-rwxr-xr-x` (executable)
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
chmod +x .claude/hooks/*.sh
|
||||
```
|
||||
|
||||
### 3. Incorrect Shebang
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
head -1 .claude/hooks/skill-activation-prompt.sh
|
||||
```
|
||||
|
||||
Expected: `#!/bin/bash`
|
||||
|
||||
**Fix:** Add correct shebang to first line
|
||||
|
||||
### 4. npx/tsx Not Available
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
npx tsx --version
|
||||
```
|
||||
|
||||
Expected: Version number
|
||||
|
||||
**Fix:** Install dependencies:
|
||||
```bash
|
||||
cd .claude/hooks
|
||||
npm install
|
||||
```
|
||||
|
||||
### 5. TypeScript Compilation Error
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
cd .claude/hooks
|
||||
npx tsc --noEmit skill-activation-prompt.ts
|
||||
```
|
||||
|
||||
Expected: No output (no errors)
|
||||
|
||||
**Fix:** Correct TypeScript syntax errors
|
||||
|
||||
---
|
||||
|
||||
## Performance Issues
|
||||
|
||||
**Symptoms:** Hooks are slow, noticeable delay before prompt/edit.
|
||||
|
||||
**Common Causes:**
|
||||
|
||||
### 1. Too Many Patterns
|
||||
|
||||
**Check:**
|
||||
- Count patterns in skill-rules.json
|
||||
- Each pattern = regex compilation + matching
|
||||
|
||||
**Solution:** Reduce patterns
|
||||
- Combine similar patterns
|
||||
- Remove redundant patterns
|
||||
- Use more specific patterns (faster matching)
|
||||
|
||||
### 2. Complex Regex
|
||||
|
||||
**Problem:**
|
||||
```regex
|
||||
(create|add|modify|update|implement|build).*?(feature|endpoint|route|service|controller|component|UI|page)
|
||||
```
|
||||
- Long alternations = slow
|
||||
|
||||
**Solution:** Simplify
|
||||
```regex
|
||||
(create|add).*?(feature|endpoint) // Fewer alternatives
|
||||
```
|
||||
|
||||
### 3. Too Many Files Checked
|
||||
|
||||
**Problem:**
|
||||
```json
|
||||
"pathPatterns": [
|
||||
"**/*.ts" // Checks ALL TypeScript files
|
||||
]
|
||||
```
|
||||
|
||||
**Solution:** Be more specific
|
||||
```json
|
||||
"pathPatterns": [
|
||||
"form/src/services/**/*.ts", // Only specific directory
|
||||
"form/src/controllers/**/*.ts"
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Large Files
|
||||
|
||||
Content pattern matching reads entire file - slow for large files.
|
||||
|
||||
**Solution:**
|
||||
- Only use content patterns when necessary
|
||||
- Consider file size limits (future enhancement)
|
||||
|
||||
### Measure Performance
|
||||
|
||||
```bash
|
||||
# UserPromptSubmit
|
||||
time echo '{"prompt":"test"}' | npx tsx .claude/hooks/skill-activation-prompt.ts
|
||||
|
||||
# PreToolUse
|
||||
time cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts
|
||||
{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}}
|
||||
EOF
|
||||
```
|
||||
|
||||
**Target metrics:**
|
||||
- UserPromptSubmit: < 100ms
|
||||
- PreToolUse: < 200ms
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
- [SKILL.md](SKILL.md) - Main skill guide
|
||||
- [HOOK_MECHANISMS.md](HOOK_MECHANISMS.md) - How hooks work
|
||||
- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Configuration reference
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"description": "Skill activation triggers for Claude Code. Controls when skills automatically suggest or block actions.",
|
||||
"skills": {
|
||||
"skill-developer": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"description": "Meta-skill for creating and managing Claude Code skills",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"skill system",
|
||||
"create skill",
|
||||
"add skill",
|
||||
"skill triggers",
|
||||
"skill rules",
|
||||
"hook system",
|
||||
"skill development",
|
||||
"skill-rules.json"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(how do|how does|explain).*?skill",
|
||||
"(create|add|modify|build).*?skill",
|
||||
"skill.*?(work|trigger|activate|system)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"backend-dev-guidelines": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"description": "Backend development patterns for Node.js/NextJS/Express/TypeScript",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"backend",
|
||||
"backend development",
|
||||
"microservice",
|
||||
"controller",
|
||||
"service",
|
||||
"repository",
|
||||
"route",
|
||||
"routing",
|
||||
"express",
|
||||
"API",
|
||||
"endpoint",
|
||||
"middleware",
|
||||
"validation",
|
||||
"Zod",
|
||||
"Prisma",
|
||||
"database access",
|
||||
"BaseController",
|
||||
"dependency injection",
|
||||
"unifiedConfig",
|
||||
"configuration"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(create|add|implement|build).*?(route|endpoint|API|controller|service|repository)",
|
||||
"(fix|handle|debug).*?(error|exception|backend)",
|
||||
"(add|implement).*?(middleware|validation|error.*?handling)",
|
||||
"(organize|structure|refactor).*?(backend|service|API)",
|
||||
"(how to|best practice).*?(backend|route|controller|service)"
|
||||
]
|
||||
},
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"worker/src/**/*.ts",
|
||||
"packages/shared/src/**/*.ts",
|
||||
"web/src/**/*.ts"
|
||||
],
|
||||
"pathExclusions": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"contentPatterns": [
|
||||
"router\\.",
|
||||
"app\\.(get|post|put|delete|patch)",
|
||||
"export.*Controller",
|
||||
"export.*Service",
|
||||
"prisma\\."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[codespell]
|
||||
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma,pnpm-lock.yaml
|
||||
ignore-words-list = afterall,vertx,notIn,alue,allTime
|
||||
ignore-words-list = afterall,vertx,notIn
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Code Review Checklist
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive checklist for conducting thorough code reviews to ensure quality, security, and maintainability.
|
||||
|
||||
## Review Categories
|
||||
|
||||
### Functionality
|
||||
|
||||
- [ ] Code does what it's supposed to do
|
||||
- [ ] Edge cases are handled
|
||||
- [ ] Error handling is appropriate
|
||||
- [ ] No obvious bugs or logic errors
|
||||
|
||||
### Code Quality
|
||||
|
||||
- [ ] Code is readable and well-structured
|
||||
- [ ] Functions are small and focused
|
||||
- [ ] Variable names are descriptive
|
||||
- [ ] No code duplication
|
||||
- [ ] Follows project conventions
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] No obvious security vulnerabilities
|
||||
- [ ] Input validation is present
|
||||
- [ ] Sensitive data is handled properly
|
||||
- [ ] No hardcoded secrets
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"agentCanUpdateSnapshot": false,
|
||||
"install": "cp .env.dev.example .env && pnpm i",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Development Terminal",
|
||||
"command": "pnpm run dev",
|
||||
"description": "Main development terminal running the development server"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
description: How to manage authorization and RBAC within full-stack langfuse features
|
||||
globs: web/
|
||||
alwaysApply: true
|
||||
---
|
||||
See [README.md](mdc:web/src/features/rbac/README.md) for details
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
description: Guidelines for positioning elements with banner awareness
|
||||
globs:
|
||||
- "**/*.tsx"
|
||||
- "**/*.jsx"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## Banner Height System
|
||||
|
||||
When positioning elements with `sticky`, `fixed`, or `absolute` that have a global reference point (like `top-0`), **always use `top-banner-offset` instead of `top-0`**.
|
||||
|
||||
### Why?
|
||||
The application can display system banners (payment issues, maintenance notices, etc.) that push content down. Using `top-banner-offset` ensures your positioned elements appear below the banner rather than being covered by it.
|
||||
|
||||
### Usage
|
||||
|
||||
**Bad:**
|
||||
```tsx
|
||||
<div className="fixed top-0 left-0 right-0">...</div>
|
||||
<div className="sticky top-0">...</div>
|
||||
<div className="absolute top-0">...</div>
|
||||
<div className="pt-0">...</div>
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```tsx
|
||||
<div className="fixed top-banner-offset left-0 right-0">...</div>
|
||||
<div className="sticky top-banner-offset">...</div>
|
||||
<div className="absolute top-banner-offset">...</div>
|
||||
<div className="pt-banner-offset">...</div>
|
||||
```
|
||||
|
||||
### Additional Utilities
|
||||
|
||||
- `h-screen-with-banner` / `min-h-screen-with-banner` - Use these for full-height containers that should account for banner space
|
||||
- `pt-banner-offset` - Use for padding-top to offset content below the banner
|
||||
- The system uses CSS variables `--banner-height` and `--banner-offset` defined in `web/src/styles/globals.css`
|
||||
- Banner components dynamically update `--banner-height` using ResizeObserver to track their actual rendered height, ensuring accuracy across all screen sizes
|
||||
|
||||
### When NOT to use `top-banner-offset`
|
||||
|
||||
- Elements positioned relative to their parent container (not globally)
|
||||
- Elements that should intentionally appear at the very top of the viewport
|
||||
- Elements inside modals/dialogs that are already z-indexed above banners
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
description: How to enforce entitlements across full-stack features
|
||||
globs: web/**
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
Check [README.md](mdc:web/src/features/entitlements/README.md) to learn more about entitlements
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
description: How we build new full-stack features including frontend APIs, file structure, and components
|
||||
globs: web/**
|
||||
alwaysApply: true
|
||||
---
|
||||
## Framework
|
||||
|
||||
- Next.js
|
||||
- Pages router
|
||||
|
||||
## File Structure
|
||||
|
||||
- We generally put all code related to a net-new feature into a folder within web/src/features.
|
||||
- Check out other features to learn about the common structure.
|
||||
|
||||
## API for frontend features
|
||||
|
||||
- We use TRPC.io to power full-stack features of the Langfuse Frontend
|
||||
- Entry point for all trpc routes: [root.ts](mdc:web/src/server/api/root.ts)
|
||||
- Authentication, see [authorization-and-rbac.mdc](mdc:.cursor/rules/authorization-and-rbac.mdc)
|
||||
- Entitlements, see [entitlements.mdc](mdc:.cursor/rules/entitlements.mdc)
|
||||
|
||||
## Components
|
||||
|
||||
- We use Shadcn/ui
|
||||
- Components in `@/src/components/ui`
|
||||
- If a component is not installed yet, ask the user to install it for you
|
||||
- When creating new custom components that generalize, we add them with a generalizable naming to `@/src/components`
|
||||
|
||||
## Styling
|
||||
|
||||
- We use Tailwind CSS
|
||||
- We use a standard color palette which automatically handles light/dark mode, see [globals.css](mdc:web/src/styles/globals.css)
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# General rules
|
||||
|
||||
- Linting in this repo only works if the development server is running
|
||||
- Always run the full mono-repo via `pnpm run dx` (use `pnpm dx-f` when you run this in a background agent). Thereby the database will also be seeded.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## Project setup
|
||||
|
||||
- This project is a Turborepo monorepo
|
||||
- We build two containers, web (./web) and worker (./worker)
|
||||
- We have shared code between these two in the shared package (./packages/shared). For that package, we have different entry points [package.json](mdc:packages/shared/package.json).
|
||||
|
||||
## Domain layer
|
||||
|
||||
The most important domain objects are in [observations.ts](mdc:packages/shared/src/domain/observations.ts), [traces.ts](mdc:packages/shared/src/domain/traces.ts), [scores.ts](mdc:packages/shared/src/domain/scores.ts).
|
||||
|
||||
## Database schema
|
||||
|
||||
We use Postgres and Clickhouse.
|
||||
|
||||
- The postgres schema is in [schema.prisma](mdc:packages/shared/prisma/schema.prisma)
|
||||
- The clickhouse schema is in [0001_traces.up.sql](mdc:packages/shared/clickhouse/migrations/clustered/0001_traces.up.sql), [0002_observations.up.sql](mdc:packages/shared/clickhouse/migrations/clustered/0002_observations.up.sql), [0003_scores.up.sql](mdc:packages/shared/clickhouse/migrations/clustered/0003_scores.up.sql)
|
||||
|
||||
## Cursor Background/Cloud Agent
|
||||
|
||||
When running in a background/cloud agent, please adhere to the following rules:
|
||||
|
||||
- Before finishing a task, please run prettier via `pnpm format` to format the code of the repository. Otherwise, the CI check will fail on the change.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
description: How to create new public api routes for Langfuse
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Implementation
|
||||
|
||||
- All public api routes are in /web/src/pages/api/public
|
||||
- New api routes should follow these guidelines:
|
||||
- Use [withMiddlewares.ts](mdc:web/src/features/public-api/server/withMiddlewares.ts) as a wrapper
|
||||
- Define types of api request and response in /web/src/features/public-api/types, use strict() for all zod objects
|
||||
- Add end-to-end test, similar to [datasets-api.servertest.ts](mdc:web/src/__tests__/async/datasets-api.servertest.ts)
|
||||
- Add fern configuration in /fern, learn more about structure here: https://buildwithfern.com/learn/api-definition/fern/overview
|
||||
- Prompt user to regenerate the OpenAPI spec via the fern CLI
|
||||
- Pagination starts at 1, query typing defined in publicApiPaginationZod, return meta in paginationMetaResponseZod
|
||||
- For tests, please look at [this directory](mdc:web/src/__tests__/async/) for examples
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Writing tests
|
||||
|
||||
- When writing tests, focus on decoupling each `it` or `test` block to ensure that they can run independently and concurrently. Tests must never depend on the action or outcome of previous or subsequent tests.
|
||||
- When writing tests, especially in the __tests__/async directory, ensure that you avoid `pruneDatabase` calls.
|
||||
@@ -1,13 +0,0 @@
|
||||
# Dev container Dockerfile
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bookworm
|
||||
|
||||
# Install golang-migrate for database migrations
|
||||
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz && \
|
||||
chmod +x migrate && \
|
||||
mv migrate /usr/local/bin/migrate
|
||||
|
||||
# Install pnpm globally
|
||||
RUN npm install -g pnpm@9.5.0
|
||||
|
||||
# Install Claude Code CLI
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "langfuse-development",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"forwardPorts": [3000, 5432, 6379, 8123, 9000],
|
||||
"postCreateCommand": "cp .env.dev.example .env && pnpm i"
|
||||
}
|
||||
+18
-11
@@ -11,7 +11,7 @@ CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
|
||||
CLICKHOUSE_URL="http://localhost:8123"
|
||||
CLICKHOUSE_USER="clickhouse"
|
||||
CLICKHOUSE_PASSWORD="clickhouse"
|
||||
CLICKHOUSE_CLUSTER_ENABLED="false"
|
||||
CLICKHOUSE_MIGRATION_CLUSTER_DISABLED="true"
|
||||
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
@@ -35,18 +35,17 @@ EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# DON'T PANIC: The Azurite Secrets are well-known and meant to be hard-coded
|
||||
# S3 Batch Exports
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
|
||||
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=devstoreaccount1
|
||||
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
LANGFUSE_S3_BATCH_EXPORT_REGION=auto
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:10000/devstoreaccount1
|
||||
# S3 storage
|
||||
S3_ENDPOINT=http://localhost:10000/devstoreaccount1
|
||||
S3_ACCESS_KEY_ID=devstoreaccount1
|
||||
S3_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
S3_BUCKET_NAME=langfuse
|
||||
S3_REGION=auto
|
||||
## Necessary for minio compatibility
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# S3 Media Upload LOCAL
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
@@ -58,6 +57,7 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
@@ -68,7 +68,6 @@ LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
|
||||
LANGFUSE_USE_AZURE_BLOB=true
|
||||
LANGFUSE_AZURE_SKIP_CONTAINER_CHECK=false
|
||||
|
||||
# Set during docker build of application
|
||||
# Used to disable environment verification at build time
|
||||
@@ -83,3 +82,11 @@ ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# speeds up local development by not executing init scripts on server startup
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
|
||||
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
|
||||
|
||||
+17
-81
@@ -1,43 +1,6 @@
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# ============================================================================
|
||||
# DOCKER CONFIGURATION
|
||||
# ============================================================================
|
||||
# These variables configure docker-compose port mappings and container names
|
||||
# To run multiple instances, copy this file to .env and customize these values
|
||||
|
||||
# Host Ports
|
||||
# POSTGRES_HOST_PORT=5432
|
||||
# REDIS_HOST_PORT=6379
|
||||
# CLICKHOUSE_HTTP_PORT=8123
|
||||
# CLICKHOUSE_NATIVE_PORT=9000
|
||||
# MINIO_API_PORT=9090
|
||||
# MINIO_CONSOLE_PORT=9091
|
||||
# WEB_HOST_PORT=3000
|
||||
# WORKER_HOST_PORT=3030
|
||||
|
||||
# Container Names
|
||||
# POSTGRES_CONTAINER_NAME=langfuse-postgres
|
||||
# CLICKHOUSE_CONTAINER_NAME=langfuse-clickhouse
|
||||
# REDIS_CONTAINER_NAME=langfuse-redis
|
||||
# MINIO_CONTAINER_NAME=langfuse-minio
|
||||
# WEB_CONTAINER_NAME=langfuse-web
|
||||
# WORKER_CONTAINER_NAME=langfuse-worker
|
||||
|
||||
# Volumes
|
||||
# POSTGRES_VOLUME_NAME=langfuse_postgres_data
|
||||
# CLICKHOUSE_DATA_VOLUME_NAME=langfuse_clickhouse_data
|
||||
# CLICKHOUSE_LOGS_VOLUME_NAME=langfuse_clickhouse_logs
|
||||
# MINIO_VOLUME_NAME=langfuse_minio_data
|
||||
|
||||
# Network
|
||||
# DOCKER_NETWORK_NAME=langfuse-network
|
||||
|
||||
# ============================================================================
|
||||
# APPLICATION CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Prisma
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
@@ -46,11 +9,9 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
# Clickhouse
|
||||
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
|
||||
CLICKHOUSE_URL="http://localhost:8123"
|
||||
# CLICKHOUSE_READ_ONLY_URL="http://localhost:8123" # Optional: read replica for legacy tables
|
||||
# CLICKHOUSE_EVENTS_READ_ONLY_URL="http://localhost:8123" # Optional: read replica for events table queries
|
||||
CLICKHOUSE_USER="clickhouse"
|
||||
CLICKHOUSE_PASSWORD="clickhouse"
|
||||
CLICKHOUSE_CLUSTER_ENABLED="false"
|
||||
CLICKHOUSE_CLUSTER_DISABLED="true"
|
||||
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
@@ -72,20 +33,18 @@ SALT="salt"
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
CLOUD_CRM_EMAIL="" # Optional BCC address for usage threshold emails (e.g., for CRM integration like HubSpot)
|
||||
|
||||
# S3 Batch Exports
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
|
||||
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=miniosecret
|
||||
LANGFUSE_S3_BATCH_EXPORT_REGION=us-east-1
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:9090
|
||||
# S3 storage
|
||||
S3_ENDPOINT=http://localhost:9090
|
||||
S3_ACCESS_KEY_ID=minio
|
||||
S3_SECRET_ACCESS_KEY=miniosecret
|
||||
S3_BUCKET_NAME=langfuse
|
||||
S3_REGION=us-east-1
|
||||
## Necessary for minio compatibility
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# S3 Media Upload LOCAL
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -97,6 +56,7 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -113,41 +73,17 @@ LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
REDIS_AUTH="myredissecret"
|
||||
# REDIS_SENTINEL_ENABLED="false"
|
||||
# REDIS_SENTINEL_NODES="sentinel1:26379,sentinel2:26379"
|
||||
# REDIS_SENTINEL_MASTER_NAME="mymaster"
|
||||
# REDIS_SENTINEL_USERNAME=""
|
||||
# REDIS_SENTINEL_PASSWORD=""
|
||||
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# speeds up local development by not executing init scripts on server startup
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
|
||||
# For SDK integration tests to pass, decrease the ingestion queue delay by uncommenting the env vars:
|
||||
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=10
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=10
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
|
||||
|
||||
# Slack credentials for development
|
||||
SLACK_CLIENT_ID=your_slack_client_id
|
||||
SLACK_CLIENT_SECRET=your_slack_client_secret
|
||||
SLACK_STATE_SECRET=your_slack_state_secret
|
||||
|
||||
# Langfuse AI instance for tracing, prompts
|
||||
LANGFUSE_AI_FEATURES_PUBLIC_KEY="pk-lf-1234567890"
|
||||
LANGFUSE_AI_FEATURES_SECRET_KEY="sk-lf-1234567890"
|
||||
LANGFUSE_AI_FEATURES_HOST="http://localhost:3000"
|
||||
LANGFUSE_AI_FEATURES_PROJECT_ID=7a88fb47-b4e2-43b8-a06c-a5ce950dc53a
|
||||
|
||||
# Langfuse AI Bedrock credentials
|
||||
AWS_ACCESS_KEY_ID="A123456789"
|
||||
AWS_SECRET_ACCESS_KEY="SAK123456789"
|
||||
LANGFUSE_AWS_BEDROCK_REGION="eu-west-1"
|
||||
LANGFUSE_AWS_BEDROCK_MODEL="eu.anthropic.claude-3-haiku-20240307-v1:0"
|
||||
|
||||
# Events table migration
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS=true
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_FLAGS=true
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS=true
|
||||
LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
|
||||
|
||||
@@ -34,18 +34,17 @@ SALT="salt"
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 Batch Exports
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
|
||||
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=miniosecret
|
||||
LANGFUSE_S3_BATCH_EXPORT_REGION=us-east-1
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:9090
|
||||
# S3 storage
|
||||
S3_ENDPOINT=http://localhost:9090
|
||||
S3_ACCESS_KEY_ID=minio
|
||||
S3_SECRET_ACCESS_KEY=miniosecret
|
||||
S3_BUCKET_NAME=langfuse
|
||||
S3_REGION=us-east-1
|
||||
## Necessary for minio compatibility
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# S3 Media Upload LOCAL
|
||||
# # S3 Media Upload LOCAL
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -57,6 +56,7 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -72,16 +72,10 @@ LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
REDIS_AUTH="bitnami"
|
||||
REDIS_CLUSTER_ENABLED="true"
|
||||
REDIS_CLUSTER_NODES="127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375"
|
||||
LANGFUSE_INGESTION_QUEUE_SHARD_COUNT=8
|
||||
LANGFUSE_OTEL_INGESTION_QUEUE_SHARD_COUNT=4
|
||||
LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT=4
|
||||
REDIS_AUTH="myredissecret"
|
||||
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# speeds up local development by not executing init scripts on server startup
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
@@ -0,0 +1,22 @@
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# Next Auth
|
||||
NEXTAUTH_SECRET="secret"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# feature flag to enable experimental features locally
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
|
||||
# Redis
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
REDIS_AUTH="myredissecret"
|
||||
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
+59
-110
@@ -41,10 +41,8 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
|
||||
OTEL_SERVICE_NAME="langfuse"
|
||||
|
||||
# Default role for users who sign up, optional, can be org or org+project
|
||||
# Supports comma-separated IDs for multiple orgs (e.g., "org1,org2,org3")
|
||||
# LANGFUSE_DEFAULT_ORG_ID=
|
||||
# LANGFUSE_DEFAULT_ORG_ROLE=
|
||||
# Supports comma-separated IDs for multiple projects (e.g., "proj1,proj2,proj3")
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
@@ -67,79 +65,44 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=
|
||||
# AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GOOGLE_ALLOWED_DOMAINS=langfuse.com,google.com # optional allowlist of workspace domains that can sign in via Google
|
||||
# AUTH_GOOGLE_CLIENT_AUTH_METHOD=
|
||||
# AUTH_GOOGLE_CHECKS=
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITHUB_CLIENT_AUTH_METHOD=
|
||||
# AUTH_GITHUB_CHECKS=
|
||||
# AUTH_GITHUB_ENTERPRISE_CLIENT_ID=
|
||||
# AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ENTERPRISE_BASE_URL=
|
||||
# AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITHUB_ENTERPRISE_CLIENT_AUTH_METHOD=
|
||||
# AUTH_GITHUB_ENTERPRISE_CHECKS=
|
||||
# AUTH_GITLAB_CLIENT_ID=
|
||||
# AUTH_GITLAB_CLIENT_SECRET=
|
||||
# AUTH_GITLAB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITLAB_ISSUER=
|
||||
# AUTH_GITLAB_CLIENT_AUTH_METHOD=
|
||||
# AUTH_GITLAB_CHECKS=
|
||||
# AUTH_GITLAB_URL=
|
||||
# AUTH_AZURE_AD_CLIENT_ID=
|
||||
# AUTH_AZURE_AD_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_TENANT_ID=
|
||||
# AUTH_AZURE_AD_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_AZURE_AD_CLIENT_AUTH_METHOD=
|
||||
# AUTH_AZURE_AD_CHECKS=
|
||||
# AUTH_AZURE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_OKTA_CLIENT_ID=
|
||||
# AUTH_OKTA_CLIENT_SECRET=
|
||||
# AUTH_OKTA_ISSUER=
|
||||
# AUTH_OKTA_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_OKTA_CLIENT_AUTH_METHOD=
|
||||
# AUTH_OKTA_CHECKS=
|
||||
# AUTH_AUTH0_CLIENT_ID=
|
||||
# AUTH_AUTH0_CLIENT_SECRET=
|
||||
# AUTH_AUTH0_ISSUER=
|
||||
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_AUTH0_CLIENT_AUTH_METHOD=
|
||||
# AUTH_AUTH0_CHECKS=
|
||||
# AUTH_COGNITO_CLIENT_ID=
|
||||
# AUTH_COGNITO_CLIENT_SECRET=
|
||||
# AUTH_COGNITO_ISSUER=
|
||||
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_COGNITO_CLIENT_AUTH_METHOD=
|
||||
# AUTH_COGNITO_CHECKS=
|
||||
# AUTH_KEYCLOAK_CLIENT_ID=
|
||||
# AUTH_KEYCLOAK_CLIENT_SECRET=
|
||||
# AUTH_KEYCLOAK_ISSUER=
|
||||
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_KEYCLOAK_CLIENT_AUTH_METHOD=
|
||||
# AUTH_KEYCLOAK_CHECKS=
|
||||
# AUTH_KEYCLOAK_NAME=
|
||||
# AUTH_WORKOS_CLIENT_ID=
|
||||
# AUTH_WORKOS_CLIENT_SECRET=
|
||||
# AUTH_WORKOS_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_WORKOS_ORGANIZATION_ID=
|
||||
# AUTH_WORKOS_CONNECTION_ID=
|
||||
# AUTH_CUSTOM_CLIENT_ID=
|
||||
# AUTH_CUSTOM_CLIENT_SECRET=
|
||||
# AUTH_CUSTOM_ISSUER=
|
||||
# AUTH_CUSTOM_NAME=
|
||||
# AUTH_CUSTOM_SCOPE="openid email profile" # optional
|
||||
# AUTH_CUSTOM_CLIENT_AUTH_METHOD="client_secret_basic" # optional
|
||||
# AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_CUSTOM_ID_TOKEN=false # optional, default is true
|
||||
# AUTH_CUSTOM_CLIENT_AUTH_METHOD=
|
||||
# AUTH_CUSTOM_CHECKS=
|
||||
# AUTH_JUMPCLOUD_CLIENT_ID=
|
||||
# AUTH_JUMPCLOUD_CLIENT_SECRET=
|
||||
# AUTH_JUMPCLOUD_ISSUER=
|
||||
# AUTH_JUMPCLOUD_ALLOW_ACCOUNT_LINKING=
|
||||
# AUTH_JUMPCLOUD_CLIENT_AUTH_METHOD=
|
||||
# AUTH_JUMPCLOUD_CHECKS=
|
||||
# AUTH_JUMPCLOUD_SCOPE=
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
@@ -147,16 +110,16 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# Defines the connection url for smtp server.
|
||||
# SMTP_CONNECTION_URL=
|
||||
|
||||
# S3 Batch Exports
|
||||
# LANGFUSE_S3_BATCH_EXPORT_ENABLED=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_BUCKET=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_REGION=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_PREFIX=
|
||||
# S3 storage, optional, used for exports from the UI
|
||||
# S3_ENDPOINT=
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION=
|
||||
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
|
||||
|
||||
# S3 storage for events, optional, used to persist all incoming events
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
|
||||
# Optional prefix to be used within the bucket. Must end with `/` if set
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
@@ -166,9 +129,10 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_REGION=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=
|
||||
# Whether to use blob_storage_file_log table to manage blob storage events
|
||||
# Can be set to `false` if `event` entities are managed using lifecycle policies in the blob storage bucket.
|
||||
LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
|
||||
|
||||
# Exports are streamed to S3 in pages to avoid memory issues
|
||||
# The page size can be adjusted if needed to optimize performance
|
||||
# DB_EXPORT_PAGE_SIZE=1000
|
||||
|
||||
# Automated provisioning of default resources
|
||||
# LANGFUSE_INIT_ORG_ID=org-id
|
||||
@@ -181,44 +145,7 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
|
||||
# LANGFUSE_INIT_USER_NAME=User Name
|
||||
# LANGFUSE_INIT_USER_PASSWORD=password
|
||||
|
||||
# Redis configuration
|
||||
# REDIS_HOST=
|
||||
# REDIS_PORT=
|
||||
# REDIS_AUTH=
|
||||
# REDIS_USERNAME=default
|
||||
# REDIS_CONNECTION_STRING=
|
||||
# REDIS_ENABLE_AUTO_PIPELINING=
|
||||
|
||||
# Redis Cluster configuration (optional)
|
||||
# REDIS_CLUSTER_ENABLED=false
|
||||
# REDIS_CLUSTER_NODES=redis-node1:6379,redis-node2:6379,redis-node3:6379
|
||||
|
||||
# Redis Sentinel configuration (optional, cannot be enabled with cluster mode simultaneously)
|
||||
# REDIS_SENTINEL_ENABLED=false
|
||||
# REDIS_SENTINEL_NODES=sentinel1:26379,sentinel2:26379,sentinel3:26379
|
||||
# REDIS_SENTINEL_MASTER_NAME=mymaster
|
||||
# REDIS_SENTINEL_USERNAME=
|
||||
# REDIS_SENTINEL_PASSWORD=
|
||||
|
||||
# Cache configuration
|
||||
# LANGFUSE_CACHE_API_KEY_ENABLED=
|
||||
# LANGFUSE_CACHE_API_KEY_TTL_SECONDS=
|
||||
# LANGFUSE_CACHE_PROMPT_ENABLED=
|
||||
# LANGFUSE_CACHE_PROMPT_TTL_SECONDS=
|
||||
|
||||
# Clickhouse configuration
|
||||
# CLICKHOUSE_URL=
|
||||
# CLICKHOUSE_CLUSTER_NAME=default
|
||||
# CLICKHOUSE_DB=default
|
||||
# CLICKHOUSE_USER=
|
||||
# CLICKHOUSE_PASSWORD=
|
||||
# CLICKHOUSE_CLUSTER_ENABLED=true
|
||||
|
||||
# Ingestion configuration
|
||||
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
|
||||
|
||||
### START Enterprise Edition Configuration
|
||||
|
||||
@@ -236,12 +163,21 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_OPENAI=https://api.openai.com/v1
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_ANTHROPIC=https://api.anthropic.com
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_AZURE_OPENAI=https://{instanceName}.openai.azure.com/openai/deployments
|
||||
# LANGFUSE_UI_VISIBLE_PRODUCT_MODULES=
|
||||
# LANGFUSE_UI_HIDDEN_PRODUCT_MODULES=
|
||||
|
||||
### END Enterprise Edition Configuration
|
||||
|
||||
|
||||
|
||||
### START Envs to be deprecated in Langfuse v3.0
|
||||
|
||||
# Disable the expensive analytics queries and related features
|
||||
# LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES="true"
|
||||
|
||||
### END Envs to be deprecated in Langfuse v3.0
|
||||
|
||||
|
||||
|
||||
|
||||
### START Langfuse Cloud Config
|
||||
# Used for Langfuse Cloud deployments
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMANTIC VERSIONING
|
||||
@@ -265,45 +201,58 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
|
||||
# SENTRY_CSP_REPORT_URI=
|
||||
|
||||
|
||||
# Cloudflare Turnstile
|
||||
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
||||
# TURNSTILE_SECRET_KEY=
|
||||
|
||||
# Demo project that users can use to try the platform
|
||||
# NEXT_PUBLIC_DEMO_ORG_ID=
|
||||
# NEXT_PUBLIC_DEMO_PROJECT_ID=
|
||||
|
||||
# Plain Chat
|
||||
# NEXT_PUBLIC_PLAIN_APP_ID=
|
||||
# PLAIN_AUTHENTICATION_SECRET=
|
||||
# PLAIN_API_KEY=
|
||||
# PLAIN_CARDS_API_TOKEN=
|
||||
# Crisp chat
|
||||
# NEXT_PUBLIC_CRISP_WEBSITE_ID=
|
||||
|
||||
# Admin API
|
||||
# ADMIN_API_KEY=
|
||||
# LANGFUSE_CACHE_MODEL_MATCH_ENABLED=
|
||||
# LANGFUSE_CACHE_MODEL_MATCH_TTL_SECONDS=
|
||||
|
||||
# Redis
|
||||
# REDIS_HOST=
|
||||
# REDIS_PORT=
|
||||
# REDIS_AUTH=
|
||||
# REDIS_CONNECTION_STRING=
|
||||
# REDIS_ENABLE_AUTO_PIPELINING=
|
||||
|
||||
# Cache configuration
|
||||
# LANGFUSE_CACHE_API_KEY_ENABLED=
|
||||
# LANGFUSE_CACHE_API_KEY_TTL_SECONDS=
|
||||
# LANGFUSE_CACHE_PROMPT_ENABLED=
|
||||
# LANGFUSE_CACHE_PROMPT_TTL_SECONDS=
|
||||
|
||||
# Rate limiting
|
||||
# LANGFUSE_RATE_LIMITS_ENABLED=
|
||||
|
||||
# Free tier usage thresholds (Cloud deployments only)
|
||||
# Enable the queue consumer that monitors free tier usage (default: true, but requires cloud region)
|
||||
# QUEUE_CONSUMER_FREE_TIER_USAGE_THRESHOLD_QUEUE_IS_ENABLED=true
|
||||
# Enable enforcement: send emails and block orgs that exceed free tier limits (default: false)
|
||||
# LANGFUSE_FREE_TIER_USAGE_THRESHOLD_ENFORCEMENT_ENABLED=false
|
||||
# Optional BCC address for usage threshold emails (e.g., for CRM integration like HubSpot)
|
||||
# CLOUD_CRM_EMAIL=
|
||||
|
||||
# Stripe
|
||||
# STRIPE_SECRET_KEY=
|
||||
# STRIPE_WEBHOOK_SIGNING_SECRET=
|
||||
|
||||
# Betterstack Status Page
|
||||
# BETTERSTACK_UPTIME_API_KEY=
|
||||
# BETTERSTACK_UPTIME_STATUS_PAGE_ID=
|
||||
|
||||
### END Langfuse Cloud Config
|
||||
|
||||
|
||||
### START Langfuse CI Config
|
||||
## START Langfuse V3 Ingestion
|
||||
|
||||
# LANGFUSE_INIT_ORG_CLOUD_PLAN=
|
||||
# Clickhouse
|
||||
# CLICKHOUSE_MIGRATION_URL=
|
||||
# CLICKHOUSE_URL=
|
||||
# CLICKHOUSE_USER=
|
||||
# CLICKHOUSE_PASSWORD=
|
||||
|
||||
### END Langfuse CI Config
|
||||
# Ingestion
|
||||
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
|
||||
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
|
||||
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
|
||||
|
||||
## END Langfuse V3 Ingestion
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Test Environment Configuration
|
||||
# Copy this file to .env.test for test database isolation
|
||||
# Only overrides specific test variables - other values inherited from .env
|
||||
|
||||
# PostgreSQL - Test Database
|
||||
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/langfuse_test"
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/langfuse_test"
|
||||
|
||||
# ClickHouse - Use Default Database for now, nothing set
|
||||
|
||||
# Redis - Test Database (database 1 for isolation)
|
||||
REDIS_CONNECTION_STRING="redis://:myredissecret@127.0.0.1:6379/1"
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
build
|
||||
coverage
|
||||
@@ -1,36 +0,0 @@
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe your question
|
||||
description: Please describe the question you have as clear and concise as possible. Include error messages, unexpected behavior, or steps to reproduce the problem.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Langfuse Cloud or Self-Hosted?
|
||||
options:
|
||||
- "Langfuse Cloud"
|
||||
- "Self-Hosted"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: If Self-Hosted
|
||||
description: What version are you running? We may ask you to upgrade to the latest version, as many issues are continuously being fixed.
|
||||
- type: input
|
||||
attributes:
|
||||
label: If Langfuse Cloud
|
||||
description: Please share the link to your Langfuse project or the specific view you have a question about. This helps us resolve requests faster.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: SDK and integration versions
|
||||
description: If you're experiencing an issue with an integration or SDK, please share all package versions you're using. If you are not on the latest version, try upgrading, as this will often resolve the issue.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Pre-Submission Checklist
|
||||
description: Please check for existing [issues](https://github.com/langfuse/langfuse/issues) and [discussions](https://github.com/orgs/langfuse/discussions) and ask the [Langfuse AI chatbot](https://langfuse.com/docs/ask-ai).
|
||||
options:
|
||||
- label: I have checked for existing issues/discussions and consulted Langfuse AI.
|
||||
required: true
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,45 +1,33 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Report a bug to help us improve
|
||||
title: "bug: <short description>"
|
||||
description: Create a bug report to help us improve
|
||||
title: "bug: "
|
||||
labels: ["🐞❔ unconfirmed bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug, and what you expected to happen when you encountered it.
|
||||
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Describe how to reproduce the bug. Please provide detailed steps, code snippets, a minimal reproduction repository, etc.
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Please provide detailed steps, code snippets, reproduction repos etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Langfuse Cloud or self-hosted?
|
||||
options:
|
||||
- "Langfuse Cloud"
|
||||
- "Self-hosted"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: If self-hosted, what version are you running?
|
||||
description: We may ask you to upgrade to the latest version, as many issues are continuously being fixed.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: SDK and integration versions
|
||||
description: If you're experiencing an issue with an integration or SDK, please share all package versions you're using. If you are not on the latest version, try upgrading, as this will often resolve the issue.
|
||||
label: SDK and container versions
|
||||
description: If you're experiencing an issue with an integration or SDK, please ensure you're using the latest version. If you're self-hosting Langfuse, check that you're running the most recent version. If updating isn't an option, please provide the specific versions you're currently using.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, including screenshots if applicable.
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
- type: dropdown
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Are you interested in contributing a fix for this bug?
|
||||
description: If this is a confirmed bug, the maintainers are happy to provide guidance and review.
|
||||
label: Are you interested to contribute a fix for this bug?
|
||||
description: If this is a confirmed bug, the maintainers are happy to support with guidance and review.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
|
||||
@@ -28,7 +28,7 @@ Fixes # (issue)
|
||||
<!-- Remove bullet points below that don't apply to you -->
|
||||
|
||||
- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
|
||||
- My code doesn't follow the style guidelines of this project (`pnpm run format`)
|
||||
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
|
||||
- I haven't commented my code, particularly in hard-to-understand areas
|
||||
- I haven't checked if my PR needs changes to the documentation
|
||||
- I haven't checked if my changes generate no new warnings (`npm run lint`)
|
||||
|
||||
@@ -36,7 +36,6 @@ updates:
|
||||
patterns:
|
||||
- "express"
|
||||
- "@types/express"
|
||||
- "@types/express-serve-static-core"
|
||||
observability:
|
||||
patterns:
|
||||
- "dd-trace"
|
||||
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
--build-arg NEXT_PUBLIC_BUILD_ID=${{ github.sha }} \
|
||||
--build-arg NEXT_PUBLIC_POSTHOG_KEY=${{ vars.NEXT_PUBLIC_POSTHOG_KEY }} \
|
||||
--build-arg NEXT_PUBLIC_POSTHOG_HOST=${{ vars.NEXT_PUBLIC_POSTHOG_HOST }} \
|
||||
--build-arg NEXT_PUBLIC_PLAIN_APP_ID=${{ vars.NEXT_PUBLIC_PLAIN_APP_ID }} \
|
||||
--build-arg NEXT_PUBLIC_CRISP_WEBSITE_ID=${{ vars.NEXT_PUBLIC_CRISP_WEBSITE_ID }} \
|
||||
--build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \
|
||||
--build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \
|
||||
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
|
||||
--build-arg NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE=${{ vars.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE }} \
|
||||
.
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
|
||||
- name: Render AWS ECS Task Definition
|
||||
id: render-task-definition
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
|
||||
@@ -6,24 +6,21 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
service:
|
||||
description: "Service to be deployed"
|
||||
description: 'Service to be deployed'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- web
|
||||
- web-ingestion
|
||||
- web-iso
|
||||
- worker
|
||||
- worker-cpu
|
||||
required: true
|
||||
environment:
|
||||
description: "Environment to deploy to"
|
||||
description: 'Environment to deploy to'
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- prod-eu
|
||||
- prod-us
|
||||
- prod-hipaa
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
@@ -45,12 +42,12 @@ jobs:
|
||||
script: |
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
if (context.payload.inputs.service === "all") {
|
||||
return `["web", "web-ingestion", "web-iso", "worker", "worker-cpu"]`
|
||||
return `["web", "web-ingestion", "worker"]`
|
||||
}
|
||||
return `["${context.payload.inputs.service}"]`
|
||||
}
|
||||
if (context.eventName === "push") {
|
||||
return `["web", "web-ingestion", "web-iso", "worker", "worker-cpu"]`
|
||||
return `["web", "web-ingestion", "worker"]`
|
||||
}
|
||||
return "[]"
|
||||
result-encoding: string
|
||||
@@ -81,7 +78,7 @@ jobs:
|
||||
return `["staging"]`
|
||||
}
|
||||
if (context.ref === "refs/heads/production") {
|
||||
return `["prod-eu", "prod-us", "prod-hipaa"]`
|
||||
return `["prod-eu", "prod-us"]`
|
||||
}
|
||||
}
|
||||
return "[]"
|
||||
|
||||
+60
-379
@@ -10,45 +10,11 @@ on:
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
pre-job:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
llm_connections_changed: ${{ steps.filter.outputs.llm_connections }}
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
do_not_skip: '["workflow_dispatch"]'
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Recommended for paths-filter action
|
||||
# may save additional git fetch roundtrip if
|
||||
# merge-base is found within latest N commits
|
||||
fetch-depth: 20
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
llm_connections:
|
||||
- 'packages/shared/package.json'
|
||||
- 'packages/shared/src/server/llm/fetchLLMCompletion.ts'
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
@@ -56,17 +22,9 @@ jobs:
|
||||
version: 9.5.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-lint-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-lint-
|
||||
${{ runner.os }}-turbo-
|
||||
- name: install dependencies
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -76,60 +34,13 @@ jobs:
|
||||
- name: lint web
|
||||
run: pnpm run lint
|
||||
|
||||
prettier-check:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
- name: install dependencies
|
||||
run: |
|
||||
pnpm i
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
- name: Check formatting on changed files
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE_SHA=${{ github.event.pull_request.base.sha }}
|
||||
else
|
||||
BASE_SHA=$(git merge-base origin/main HEAD)
|
||||
fi
|
||||
|
||||
echo "Checking files changed from $BASE_SHA to HEAD"
|
||||
|
||||
# Get changed files
|
||||
CHANGED_FILES=$(git diff --name-only $BASE_SHA HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css' | tr '\n' ' ')
|
||||
|
||||
if [ -n "$CHANGED_FILES" ] && [ "$CHANGED_FILES" != " " ]; then
|
||||
echo "Files to check: $CHANGED_FILES"
|
||||
pnpm prettier --check --experimental-cli $CHANGED_FILES
|
||||
else
|
||||
echo "No JS/TS/CSS files changed - skipping prettier check"
|
||||
fi
|
||||
|
||||
test-docker-build:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -138,9 +49,7 @@ jobs:
|
||||
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
- name: Build and run both images from compose
|
||||
run: |
|
||||
docker compose --progress plain --verbose -f docker-compose.build.yml build --print > /tmp/bake.json
|
||||
docker buildx bake -f /tmp/bake.json
|
||||
docker compose --progress plain -f docker-compose.build.yml up -d
|
||||
docker compose -f docker-compose.build.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
- name: Ensure no unhealthy status
|
||||
run: |
|
||||
@@ -158,28 +67,28 @@ jobs:
|
||||
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
|
||||
|
||||
tests-web-sync:
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
name: tests-web-sync (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [24]
|
||||
node-version: [20]
|
||||
postgres-version: [12, 15]
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -196,48 +105,25 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
|
||||
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
|
||||
echo "LANGFUSE_CACHE_PROMPT_ENABLED=false" >> .env
|
||||
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
- name: Cache Next.js builds
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
${{ github.workspace }}/web/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('web/**/*.js', 'web/**/*.jsx', 'web/**/*.ts', 'web/**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-nextjs-${{ matrix.node-version }}-
|
||||
- name: Run dev containers
|
||||
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.example > .env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
docker compose ps
|
||||
env:
|
||||
POSTGRES_VERSION: ${{ matrix.postgres-version }}
|
||||
- name: Migrate DB
|
||||
- name: Seed DB
|
||||
run: |
|
||||
pnpm run db:migrate
|
||||
pnpm --filter=shared ch:up
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
- name: Start Langfuse
|
||||
run: (pnpm run start&)
|
||||
env:
|
||||
LANGFUSE_INIT_ORG_ID: "seed-org-id"
|
||||
LANGFUSE_INIT_ORG_NAME: "Seed Org"
|
||||
LANGFUSE_INIT_ORG_CLOUD_PLAN: "Team"
|
||||
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
|
||||
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
|
||||
@@ -246,37 +132,32 @@ jobs:
|
||||
LANGFUSE_INIT_USER_NAME: "Demo User"
|
||||
LANGFUSE_INIT_USER_PASSWORD: "password"
|
||||
- name: run test-sync
|
||||
working-directory: web
|
||||
run: npx cross-env NODE_OPTIONS='--no-experimental-require-module' npx dotenv -e ../.env.test -e ../.env -- npx jest --verbose --runInBand --detectOpenHandles --selectProjects sync-server
|
||||
- name: run test-client
|
||||
working-directory: web
|
||||
run: npx cross-env NODE_OPTIONS='--no-experimental-require-module' npx dotenv -e ../.env.test -e ../.env -- npx jest --verbose --runInBand --detectOpenHandles --selectProjects client
|
||||
run: pnpm --filter=web run test-sync
|
||||
|
||||
tests-web-async:
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
name: tests-web-async (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }}, shard${{ matrix.shard }}/3)
|
||||
name: tests-web-async (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.blob-provider }})
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [24]
|
||||
node-version: [20]
|
||||
postgres-version: [12, 15]
|
||||
deploy-mode: ["", "-azure", "-redis-cluster"]
|
||||
shard: [1, 2, 3]
|
||||
blob-provider: ["", "-azure"]
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -292,65 +173,26 @@ jobs:
|
||||
pnpm install
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev${{ matrix.deploy-mode }}.example .env
|
||||
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.deploy-mode }}.example > .env
|
||||
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
|
||||
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
|
||||
echo "LANGFUSE_TRACE_DELETE_CONCURRENCY=100" >> .env
|
||||
echo "ADMIN_API_KEY=admin-api-key" >> .env
|
||||
echo "LANGFUSE_EE_LICENSE_KEY=langfuse_ee_test" >> .env
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
- name: Cache Next.js builds
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
${{ github.workspace }}/web/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-async-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('web/**/*.js', 'web/**/*.jsx', 'web/**/*.ts', 'web/**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-async-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-nextjs-async-${{ matrix.node-version }}-
|
||||
- name: Run dev containers
|
||||
cp .env.dev${{ matrix.blob-provider }}.example .env
|
||||
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev${{ matrix.deploy-mode }}.yml up -d
|
||||
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
docker compose ps
|
||||
env:
|
||||
POSTGRES_VERSION: ${{ matrix.postgres-version }}
|
||||
- name: Migrate DB
|
||||
- name: Seed DB
|
||||
run: |
|
||||
pnpm run db:migrate
|
||||
pnpm --filter=shared ch:up
|
||||
- name: Download and extract ClickHouse client for dev-tables setup
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
CH_VERSION="24.9.3.128"
|
||||
ARCH="amd64"
|
||||
wget "https://packages.clickhouse.com/deb/pool/main/c/clickhouse/clickhouse-common-static_${CH_VERSION}_${ARCH}.deb" -O clickhouse-client.deb
|
||||
|
||||
# The binary is usually located in ./usr/bin/clickhouse in the archive
|
||||
dpkg-deb -x clickhouse-client.deb ch_client_dir
|
||||
sudo cp ch_client_dir/usr/bin/clickhouse /usr/local/bin/
|
||||
- name: Setup Dev Tables
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
pnpm --filter=shared ch:dev-tables
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
- name: Start Langfuse
|
||||
run: (pnpm run start&)
|
||||
env:
|
||||
LANGFUSE_INIT_ORG_ID: "seed-org-id"
|
||||
LANGFUSE_INIT_ORG_NAME: "Seed Org"
|
||||
LANGFUSE_INIT_ORG_CLOUD_PLAN: "Team"
|
||||
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
|
||||
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
|
||||
@@ -359,28 +201,27 @@ jobs:
|
||||
LANGFUSE_INIT_USER_NAME: "Demo User"
|
||||
LANGFUSE_INIT_USER_PASSWORD: "password"
|
||||
- name: run tests
|
||||
working-directory: web
|
||||
run: npx cross-env NODE_OPTIONS='--no-experimental-require-module' npx dotenv -e ../.env.test -e ../.env -- npx jest --verbose --runInBand --detectOpenHandles --selectProjects async-server --shard=${{ matrix.shard }}/3
|
||||
run: pnpm --filter=web run test
|
||||
|
||||
tests-worker:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
name: tests-worker (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }})
|
||||
name: tests-worker (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.blob-provider }})
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [24]
|
||||
node-version: [20]
|
||||
postgres-version: [12, 15]
|
||||
deploy-mode: ["", "-azure", "-redis-cluster"]
|
||||
blob-provider: ["", "-azure"]
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -396,17 +237,17 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev${{ matrix.deploy-mode }}.example .env
|
||||
cp .env.dev${{ matrix.deploy-mode }}.example web/.env
|
||||
cp .env.dev${{ matrix.deploy-mode }}.example worker/.env
|
||||
cp .env.dev${{ matrix.blob-provider }}.example .env
|
||||
cp .env.dev${{ matrix.blob-provider }}.example web/.env
|
||||
cp .env.dev${{ matrix.blob-provider }}.example worker/.env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev${{ matrix.deploy-mode }}.yml up -d
|
||||
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
docker compose ps
|
||||
- name: Ensure no unhealthy status
|
||||
@@ -422,101 +263,13 @@ jobs:
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run --filter=shared ch:up
|
||||
- name: Download and extract ClickHouse client for dev-tables setup
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
CH_VERSION="24.9.3.128"
|
||||
ARCH="amd64"
|
||||
wget "https://packages.clickhouse.com/deb/pool/main/c/clickhouse/clickhouse-common-static_${CH_VERSION}_${ARCH}.deb" -O clickhouse-client.deb
|
||||
dpkg-deb -x clickhouse-client.deb ch_client_dir
|
||||
sudo cp ch_client_dir/usr/bin/clickhouse /usr/local/bin/
|
||||
- name: Setup Dev Tables
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
pnpm --filter=shared ch:dev-tables
|
||||
- name: Build
|
||||
run: pnpm --filter=worker... run build
|
||||
- name: run tests
|
||||
run: pnpm --filter=worker run test:exclude-llm-connections
|
||||
|
||||
test-worker-llm-connections:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: startsWith(github.ref, 'refs/tags/') || (needs.pre-job.outputs.should_skip != 'true' && (needs.pre-job.outputs.llm_connections_changed == 'true' || github.event_name == 'workflow_dispatch'))
|
||||
name: test-worker-llm-connections (node24, pg15)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
- name: install dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
cp .env.dev.example web/.env
|
||||
cp .env.dev.example worker/.env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
docker compose ps
|
||||
env:
|
||||
POSTGRES_VERSION: 15
|
||||
- name: Ensure no unhealthy status
|
||||
run: |
|
||||
if docker compose ps | grep "(unhealthy)"; then
|
||||
echo "One or more services are unhealthy"
|
||||
exit 1
|
||||
else
|
||||
echo "All services are healthy"
|
||||
fi
|
||||
- name: Seed DB
|
||||
run: |
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run --filter=shared ch:up
|
||||
- name: Build
|
||||
run: pnpm --filter=worker... run build
|
||||
- name: run llm connection tests
|
||||
run: pnpm --filter=worker run test:llm-connections-only
|
||||
env:
|
||||
LANGFUSE_LLM_CONNECTION_OPENAI_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_OPENAI_KEY }}
|
||||
LANGFUSE_LLM_CONNECTION_ANTHROPIC_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_ANTHROPIC_KEY }}
|
||||
LANGFUSE_LLM_CONNECTION_AZURE_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_AZURE_KEY }}
|
||||
LANGFUSE_LLM_CONNECTION_AZURE_BASE_URL: ${{ secrets.LANGFUSE_LLM_CONNECTION_AZURE_BASE_URL }}
|
||||
LANGFUSE_LLM_CONNECTION_AZURE_MODEL: ${{ secrets.LANGFUSE_LLM_CONNECTION_AZURE_MODEL }}
|
||||
LANGFUSE_LLM_CONNECTION_BEDROCK_ACCESS_KEY_ID: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_ACCESS_KEY_ID }}
|
||||
LANGFUSE_LLM_CONNECTION_BEDROCK_SECRET_ACCESS_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_SECRET_ACCESS_KEY }}
|
||||
LANGFUSE_LLM_CONNECTION_BEDROCK_REGION: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_REGION }}
|
||||
LANGFUSE_LLM_CONNECTION_VERTEXAI_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_VERTEXAI_KEY }}
|
||||
LANGFUSE_LLM_CONNECTION_GOOGLEAISTUDIO_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_GOOGLEAISTUDIO_KEY }}
|
||||
- name: run tests
|
||||
run: pnpm --filter=worker run test
|
||||
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
@@ -524,33 +277,14 @@ jobs:
|
||||
version: 9.5.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-e2e-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-e2e-
|
||||
${{ runner.os }}-turbo-
|
||||
- name: Cache Next.js builds
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
${{ github.workspace }}/web/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-e2e-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('web/**/*.js', 'web/**/*.jsx', 'web/**/*.ts', 'web/**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-e2e-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-nextjs-e2e-
|
||||
- name: install dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
@@ -558,33 +292,17 @@ jobs:
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
cp .env.dev.example web/.env
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
docker compose ps
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
- name: Ensure Docker dependencies are healthy
|
||||
run: |
|
||||
if docker-compose ps | grep "(unhealthy)"; then
|
||||
echo "One or more services are unhealthy"
|
||||
exit 1
|
||||
else
|
||||
echo "All services are healthy"
|
||||
fi
|
||||
- name: Seed DB
|
||||
run: |
|
||||
pnpm run db:migrate
|
||||
pnpm --filter=shared run ch:up
|
||||
pnpm run db:seed
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
- name: Install playwright
|
||||
run: pnpm --filter=web exec playwright install --with-deps
|
||||
- name: Run e2e tests
|
||||
@@ -592,13 +310,9 @@ jobs:
|
||||
|
||||
e2e-server-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-job
|
||||
if: needs.pre-job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -608,7 +322,7 @@ jobs:
|
||||
version: 9.5.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
- name: install dependencies
|
||||
@@ -616,12 +330,16 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
echo "LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING=true" >> .env
|
||||
echo "LANGFUSE_ASYNC_INGESTION_PROCESSING=true" >> .env
|
||||
echo "LANGFUSE_CACHE_API_KEY_ENABLED=true" >> .env
|
||||
echo "LANGFUSE_CACHE_PROMPT_ENABLED=true" >> .env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
@@ -642,8 +360,6 @@ jobs:
|
||||
pnpm run db:seed:examples
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
- name: Run server
|
||||
run: (pnpm run start&)
|
||||
- name: Check worker health
|
||||
@@ -661,51 +377,27 @@ jobs:
|
||||
needs:
|
||||
[
|
||||
lint,
|
||||
prettier-check,
|
||||
tests-web-sync,
|
||||
tests-worker,
|
||||
test-worker-llm-connections,
|
||||
# e2e-tests, # Temporarily disabled - flaky timeouts
|
||||
e2e-tests,
|
||||
test-docker-build,
|
||||
e2e-server-tests,
|
||||
tests-web-async,
|
||||
]
|
||||
if: always()
|
||||
outputs:
|
||||
success: ${{ steps.set-success-output.outputs.success }}
|
||||
steps:
|
||||
- name: Set check result as an output
|
||||
id: set-success-output
|
||||
run: |
|
||||
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
|
||||
echo "success=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "success=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Successful deploy
|
||||
if: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
run: exit 0
|
||||
working-directory: .
|
||||
- name: Failing deploy
|
||||
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
||||
if: ${{ contains(needs.*.result, 'failure') }}
|
||||
run: exit 1
|
||||
working-directory: .
|
||||
- name: Notify Slack
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
if: always() && github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: "failure"
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
- name: Output job results
|
||||
run: |
|
||||
echo "Job results: ${{ steps.set-success-output.outputs.success }}"
|
||||
|
||||
push-docker-image:
|
||||
needs: all-ci-passed
|
||||
# if something inside all-ci-passed was skipped, but everything that ran passed, we still want to deploy
|
||||
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
environment: "protected branches"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -719,7 +411,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -756,9 +448,9 @@ jobs:
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
|
||||
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
|
||||
- name: Build and push Docker image (web)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -777,16 +469,13 @@ jobs:
|
||||
images: |
|
||||
ghcr.io/langfuse/langfuse-worker # GitHub
|
||||
langfuse/langfuse-worker # Docker Hub
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
|
||||
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Build and push Docker image (worker)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -798,11 +487,3 @@ jobs:
|
||||
platforms: |
|
||||
linux/amd64
|
||||
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
|
||||
- name: Notify Slack
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
if: always()
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: "failure"
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
name: SDK API Spec Generation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'fern/**'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: sdk-api-spec-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
generate-sdk-api-specs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Generate SDK specs
|
||||
env:
|
||||
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
|
||||
run: npx fern-api generate --api server --force
|
||||
|
||||
- name: Update Python SDK
|
||||
env:
|
||||
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
run: |
|
||||
# Clone langfuse-python repository
|
||||
git clone https://github.com/langfuse/langfuse-python.git ../langfuse-python
|
||||
cd ../langfuse-python
|
||||
|
||||
git config user.name "langfuse-bot"
|
||||
git config user.email "langfuse-bot@langfuse.com"
|
||||
echo "$GH_ACCESS_TOKEN" | gh auth login --with-token
|
||||
gh auth setup-git
|
||||
|
||||
# Delete existing API contents
|
||||
rm -rf langfuse/api/*
|
||||
|
||||
# Copy generated Python SDK files
|
||||
cp -r ../langfuse/generated/python/* langfuse/api/
|
||||
|
||||
# Remove unnecessary integration tests created by Fern
|
||||
# (could not find the right option to prevent this in generation step)
|
||||
rm -rf langfuse/api/tests
|
||||
|
||||
# Install poetry and format
|
||||
pip install poetry
|
||||
poetry install --all-extras
|
||||
poetry run ruff format langfuse/api
|
||||
|
||||
# Check for changes and show diff for debugging
|
||||
if git diff --quiet; then
|
||||
echo "No changes detected in Python SDK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Close existing api-spec-bot PRs
|
||||
gh pr list --author langfuse-bot --state open --json number --jq '.[].number' | xargs -I {} gh pr close {}
|
||||
|
||||
# Get the GitHub username of the original commit author
|
||||
cd ../langfuse
|
||||
ORIGINAL_AUTHOR=$(gh api repos/langfuse/langfuse/commits/${GITHUB_SHA} --jq '.author.login')
|
||||
cd ../langfuse-python
|
||||
|
||||
# Create new branch and push changes
|
||||
BRANCH_NAME="api-spec-bot-${GITHUB_SHA::7}"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
git add .
|
||||
git commit -m "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}"
|
||||
git push origin "$BRANCH_NAME"
|
||||
|
||||
# Create PR with original author as reviewer
|
||||
gh pr create --title "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" --body "" --reviewer "$ORIGINAL_AUTHOR"
|
||||
|
||||
- name: Update TypeScript SDK
|
||||
env:
|
||||
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
run: |
|
||||
# Clone langfuse-js repository
|
||||
git clone https://github.com/langfuse/langfuse-js.git ../langfuse-js
|
||||
cd ../langfuse-js
|
||||
|
||||
# Configure git
|
||||
git config user.name "langfuse-bot"
|
||||
git config user.email "langfuse-bot@langfuse.com"
|
||||
echo "$GH_ACCESS_TOKEN" | gh auth login --with-token
|
||||
gh auth setup-git
|
||||
|
||||
# Delete existing API contents
|
||||
rm -rf packages/core/src/api/*
|
||||
|
||||
# Copy generated TypeScript SDK files
|
||||
cp -r ../langfuse/generated/typescript/* packages/core/src/api/
|
||||
|
||||
# Patch compilation error inducing output
|
||||
# Without this patch, the JS SDK will not build due to
|
||||
# "Error: packages/core build: src/api/core/auth/index.ts(1,10): error TS1205: Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'."
|
||||
if [ -f "packages/core/src/api/core/auth/index.ts" ]; then
|
||||
if grep -q '^export { AuthProvider } from "./AuthProvider.js";$' packages/core/src/api/core/auth/index.ts; then
|
||||
sed -i '1s/^export { AuthProvider }/export { type AuthProvider }/' packages/core/src/api/core/auth/index.ts
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install dependencies and format
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
pnpm format
|
||||
|
||||
# Check for changes and show diff for debugging
|
||||
if git diff --quiet; then
|
||||
echo "No changes detected in TypeScript SDK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Close existing api-spec-bot PRs
|
||||
gh pr list --author langfuse-bot --state open --json number --jq '.[].number' | xargs -I {} gh pr close {}
|
||||
|
||||
# Get the GitHub username of the original commit author
|
||||
cd ../langfuse
|
||||
ORIGINAL_AUTHOR=$(gh api repos/langfuse/langfuse/commits/${GITHUB_SHA} --jq '.author.login')
|
||||
cd ../langfuse-js
|
||||
|
||||
# Create new branch and push changes
|
||||
BRANCH_NAME="api-spec-bot-${GITHUB_SHA::7}"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
git add .
|
||||
git commit -m "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" --no-verify
|
||||
git push origin "$BRANCH_NAME"
|
||||
|
||||
# Create PR with original author as reviewer
|
||||
gh pr create --title "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" --body "" --reviewer "$ORIGINAL_AUTHOR"
|
||||
+28
-10
@@ -1,28 +1,46 @@
|
||||
name: Snyk Container
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
snyk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build a Docker image
|
||||
run: docker compose -f docker-compose.build.yml up -d
|
||||
|
||||
- name: Scan web image with Snyk
|
||||
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-server)
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/docker@master
|
||||
continue-on-error: true # let upload step always run
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse/langfuse # pulled from Docker Hub
|
||||
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
|
||||
image: langfuse-server
|
||||
args: --file=web/Dockerfile
|
||||
|
||||
- name: Scan worker image with Snyk
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
category: web
|
||||
|
||||
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-worker)
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/docker@master
|
||||
continue-on-error: true # let upload step always run
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse/langfuse-worker # pulled from Docker Hub
|
||||
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
|
||||
image: langfuse-worker
|
||||
args: --file=worker/Dockerfile
|
||||
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
category: worker
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale. Please reopen if the issue persists."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
name: "Validate PR Title"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
validate-pr-title:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Validate PR title follows conventional commits
|
||||
uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# Configure the types of commits allowed
|
||||
types: |
|
||||
feat
|
||||
fix
|
||||
docs
|
||||
style
|
||||
refactor
|
||||
perf
|
||||
test
|
||||
build
|
||||
ci
|
||||
chore
|
||||
revert
|
||||
security
|
||||
# Allow scopes (optional)
|
||||
requireScope: false
|
||||
# Allow any scope (not just the ones listed above)
|
||||
validateSingleCommit: false
|
||||
ignoreLabels: |
|
||||
bot
|
||||
ignore-semantic-pull-request
|
||||
+6
-23
@@ -4,12 +4,9 @@
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
migrate
|
||||
/.pnpm-store
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/certs/
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
@@ -17,7 +14,6 @@ migrate
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.next-check/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
@@ -36,16 +32,15 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
.eslintcache
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env*
|
||||
!.env.dev.example
|
||||
!.env.dev-azure.example
|
||||
!.env.dev-redis-cluster.example
|
||||
!.env.local.example
|
||||
!.env.prod.example
|
||||
!.env.test.example
|
||||
!.env.dev.legacy.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -53,31 +48,19 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
/generated
|
||||
/generated/typescript-server
|
||||
|
||||
# openapi spec that is copied during build
|
||||
/public/openapi*.yml
|
||||
|
||||
# claude meta files
|
||||
.claude/tsc-cache
|
||||
|
||||
# vscode
|
||||
.devcontainer
|
||||
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.next/*
|
||||
**/.next-check/*
|
||||
**/.turbo/*
|
||||
.yarn
|
||||
.turbo
|
||||
|
||||
web/test-results/*
|
||||
|
||||
# local config files
|
||||
*.local.*
|
||||
|
||||
# claude meta files
|
||||
.claude/tsc-cache
|
||||
|
||||
# Refactoring planning files (local only)
|
||||
**/.refactor/
|
||||
.turbo
|
||||
+3
-4
@@ -11,13 +11,12 @@ if [ "$current_branch" = "$protected_branch" ]; then
|
||||
echo "🚨 You are about to commit to the $protected_branch branch. Are you sure? (y/n)"
|
||||
read -r answer < /dev/tty
|
||||
if [ "$answer" != "${answer#[Yy]}" ]; then
|
||||
# Commit approved, check formatting. On files changed, block commit
|
||||
pnpm run format:check
|
||||
exit 0 # Commit will proceed
|
||||
else
|
||||
echo "Commit to $protected_branch branch has been canceled."
|
||||
exit 1 # Commit will be blocked
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not the protected branch, check formatting (on changed files, block)
|
||||
pnpm run format:check
|
||||
# If not the protected branch, proceed with the commit
|
||||
exit 0
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
public-hoist-pattern[]=*prisma*
|
||||
# for turbopack, ongoing issue see: https://github.com/vercel/next.js/issues/68805
|
||||
public-hoist-pattern[]=@aws-sdk/client-s3
|
||||
public-hoist-pattern[]=*prisma*
|
||||
@@ -1,2 +0,0 @@
|
||||
# Generated code
|
||||
packages/shared/prisma/generated
|
||||
Vendored
+1
-7
@@ -28,11 +28,5 @@
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
},
|
||||
"eslint.lintTask.enable": true,
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./web" },
|
||||
{ "pattern": "./worker" },
|
||||
{ "pattern": "./packages/shared" },
|
||||
{ "pattern": "./ee" }
|
||||
],
|
||||
"eslint.useFlatConfig": true
|
||||
"eslint.workingDirectories": ["./web", "./worker"]
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Codex Guidelines for Langfuse
|
||||
|
||||
Langfuse is an **open source LLM engineering** platform for developing, monitoring, evaluating and debugging AI applications. See the README for more details.
|
||||
|
||||
## Linting
|
||||
- Run `pnpm run lint` to lint all packages.
|
||||
- Fix issues automatically with `pnpm run lint:fix`.
|
||||
|
||||
## Tests
|
||||
- Codex cannot run the test suite because it depends on Docker-based infrastructure that is unavailable in this environment.
|
||||
- When writing tests, focus on decoupling each `it` or `test` block to ensure that they can run independently and concurrently. Tests must never depend on the action or outcome of previous or subsequent tests.
|
||||
- When writing tests, especially in the __tests__/async directory, ensure that you avoid `pruneDatabase` calls.
|
||||
|
||||
## Cursor Rules
|
||||
- Additional folder-specific rules live in `.cursor/rules/`.
|
||||
|
||||
## Commits
|
||||
- Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) when crafting commit messages.
|
||||
@@ -1,217 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Project Overview
|
||||
|
||||
Langfuse is an open-source LLM engineering platform that helps teams collaboratively develop, monitor, evaluate, and debug AI applications.
|
||||
The main feature areas are tracing, evals and prompt management. Langfuse consists of the web application (this repo), documentation, python SDK and javascript/typescript SDK.
|
||||
This repo contains the web application, worker, and supporting packages but notably not the JS nor Python client SDKs.
|
||||
|
||||
## Repository Structure
|
||||
High level structure. There are more folders (eg for hooks etc).
|
||||
```
|
||||
langfuse/
|
||||
├── web/ # Next.js 14 frontend/backend application
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # Reusable UI components (shadcn/ui)
|
||||
│ │ ├── features/ # Feature-specific code organized by domain
|
||||
│ │ ├── pages/ # Next.js pages (Pages Router)
|
||||
│ │ └── server/ # tRPC API routes and server logic
|
||||
│ └── public/ # Static assets
|
||||
├── worker/ # Express.js background job processor
|
||||
│ └── src/
|
||||
│ ├── queues/ # BullMQ job queues
|
||||
│ └── services/ # Background processing services
|
||||
├── packages/
|
||||
│ ├── shared/ # Shared types, schemas, and utilities
|
||||
│ │ ├── prisma/ # Database schema and migrations
|
||||
│ │ └── src/ # Shared TypeScript code
|
||||
│ ├── config-eslint/ # ESLint configuration
|
||||
│ └── config-typescript/ # TypeScript configuration
|
||||
├── ee/ # Enterprise Edition features
|
||||
├── fern/ # API documentation and OpenAPI specs
|
||||
├── generated/ # Auto-generated client code
|
||||
└── scripts/ # Development and deployment scripts
|
||||
```
|
||||
|
||||
## Repository Architecture
|
||||
This is a **pnpm + Turbo monorepo** with the following key packages:
|
||||
|
||||
### Core Applications
|
||||
- **`/web/`** - Next.js 14 application (Pages Router) providing both frontend UI and backend APIs
|
||||
- **`/worker/`** - Express.js background job processing server
|
||||
- **`/packages/shared/`** - Shared database schema, types, and utilities
|
||||
|
||||
### Supporting Packages
|
||||
- **`/ee/`** - Enterprise Edition features (separate licensing)
|
||||
- **`/packages/config-eslint/`** - Shared ESLint configuration
|
||||
- **`/packages/config-typescript/`** - Shared TypeScript configuration
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Development
|
||||
```sh
|
||||
pnpm i # Install dependencies
|
||||
pnpm run dev # Start all services (web + worker)
|
||||
pnpm run dev:web # Web app only (localhost:3000) - **used in most cases!**
|
||||
pnpm run dev:worker # Worker only
|
||||
pnpm run dx # Full initial setup: install deps, reset DBs, resets node modules, seed data, start dev. USE SPARINGLY AS IT WIPES THE DATABASE & node_modules
|
||||
```
|
||||
|
||||
### Database Management
|
||||
database commands are to be run in the `packages/shared/` folder.
|
||||
```sh
|
||||
pnpm run db:generate # Build prisma models
|
||||
pnpm run db:migrate # Run Prisma migrations
|
||||
pnpm run db:reset # Reset and reseed databases
|
||||
pnpm run db:seed # Seed with example data
|
||||
```
|
||||
|
||||
### Infrastructure
|
||||
```sh
|
||||
pnpm run infra:dev:up # Start Docker services (PostgreSQL, ClickHouse, Redis, MinIO)
|
||||
pnpm run infra:dev:down # Stop Docker services
|
||||
```
|
||||
|
||||
### Building & Type Checking
|
||||
```sh
|
||||
pnpm --filter=PACKAGE_NAME run build # Runs the build command, will show real typescript errors etc.
|
||||
pnpm tc # Fast typecheck across all packages (alias for pnpm typecheck)
|
||||
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
|
||||
```
|
||||
|
||||
### Testing in Web Package
|
||||
The web package uses JEST for unit tests.
|
||||
Depending on the file location (sync, async)
|
||||
`web` related tests must go into the `web/src/__tests__/` folder.
|
||||
```sh
|
||||
pnpm test-sync --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
# For tests in the async folder:
|
||||
pnpm test -- --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
# For client tests:
|
||||
pnpm test-client --testPathPatterns="buildStepData" --testNamePattern="buildStepData"
|
||||
```
|
||||
|
||||
### Testing in the Worker Package
|
||||
The worker uses `vitest` for unit tests.
|
||||
```sh
|
||||
pnpm run test --filter=worker -- $TEST_FILE_NAME -t "$TEST_NAME"
|
||||
```
|
||||
|
||||
### Utilities
|
||||
```bash
|
||||
pnpm run format # Format code across entire project
|
||||
pnpm run nuke # Remove all node_modules, build files, wipe database, docker containers. **USE WITH CAUTION**
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Web Application (`/web/`)
|
||||
- **Framework**: Next.js 14 (Pages Router)
|
||||
- **APIs**: tRPC (type-safe client-server communication) + REST APIs for public access
|
||||
- **Authentication**: NextAuth.js/Auth.js
|
||||
- **Database**: Prisma ORM with PostgreSQL
|
||||
- **Analytics Database**: ClickHouse (high-volume trace data)
|
||||
- **Validation**: Zod schemas, we use zodv4 (always import from `zod/v4`)
|
||||
- **Styling**: Tailwind CSS with CSS variables for theming
|
||||
- **Components**: shadcn/ui (Radix UI primitives)
|
||||
- **State Management**: TanStack Query (React Query) + tRPC
|
||||
- **Charts**: Tremor, Recharts
|
||||
|
||||
### Worker Application (`/worker/`)
|
||||
- **Framework**: Express.js
|
||||
- **Queue System**: BullMQ with Redis
|
||||
- **Purpose**: Async processing (data ingestion, evaluations, exports, integrations)
|
||||
|
||||
### Infrastructure
|
||||
- **Primary Database**: PostgreSQL (via Prisma ORM)
|
||||
- **Analytics Database**: ClickHouse
|
||||
- **Cache/Queues**: Redis
|
||||
- **Blob Storage**: MinIO/S3
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Frontend Features
|
||||
- All new features go in `/web/src/features/[feature-name]/`
|
||||
- Use tRPC for full-stack features (entry point: `web/src/server/api/root.ts`)
|
||||
- Follow existing feature structure for consistency
|
||||
- Use shadcn/ui components from `@/src/components/ui`
|
||||
- Custom reusable components go in `@/src/components`
|
||||
|
||||
### Public API Development
|
||||
- All public API routes in `/web/src/pages/api/public`
|
||||
- Use `withMiddlewares.ts` wrapper
|
||||
- Define types in `/web/src/features/public-api/types` with strict Zod v4 objects
|
||||
- Add end-to-end tests (see `datasets-api.servertest.ts`)
|
||||
- Manually update Fern API specs in `/fern/`, then regenerate OpenAPI spec via Fern CLI
|
||||
|
||||
### Authorization & RBAC
|
||||
- Check `/web/src/features/rbac/README.md` for authorization patterns
|
||||
- Implement proper entitlements checking (see `/web/src/features/entitlements/README.md`)
|
||||
|
||||
### Database
|
||||
- **Dual database system**: PostgreSQL (primary) + ClickHouse (analytics)
|
||||
- Use `golang-migrate` CLI for database migrations
|
||||
- All database operations go through Prisma ORM for PostgreSQL
|
||||
- Foreign key relationships may not be enforced in schema to allow unordered ingestion
|
||||
|
||||
### Testing
|
||||
- Jest for API tests, Playwright for E2E tests
|
||||
- For backend/API changes, tests must pass before pushes
|
||||
- Add tests for new API endpoints and features
|
||||
- When writing tests, focus on decoupling each `it` or `test` block to ensure that they can run independently and concurrently. Tests must never depend on the action or outcome of previous or subsequent tests.
|
||||
- When writing tests, especially in the __tests__/async directory, ensure that you avoid `pruneDatabase` calls.
|
||||
|
||||
### Code Conventions
|
||||
- **Pages Router** (not App Router)
|
||||
- Follow conventional commits on main branch
|
||||
- Use CSS variables for theming (supports auto dark/light mode)
|
||||
- TypeScript throughout
|
||||
- Zod v4 for all input validation
|
||||
|
||||
## Environment Setup
|
||||
|
||||
- **Node.js**: Version 24 (specified in `.nvmrc`)
|
||||
- **Package Manager**: pnpm v9.5.0
|
||||
- **Database Dependencies**: Docker for local PostgreSQL, ClickHouse, Redis, MinIO
|
||||
- **Environment**: Copy `.env.dev.example` to `.env`
|
||||
|
||||
## Login for Development
|
||||
|
||||
When running locally with seed data:
|
||||
- Username: `demo@langfuse.com`
|
||||
- Password: `password`
|
||||
- Demo project URL: `http://localhost:3000/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a`
|
||||
|
||||
## Linear MCP
|
||||
To get a project, use the `get_project` capability with the full project name as it is in the title.
|
||||
- bad: message-placeholder-in-chat-messages-2beb6f02ec48
|
||||
- good: Message placeholder in chat messages
|
||||
|
||||
## Front-end Tips
|
||||
|
||||
### Window Location Handling
|
||||
- Whenever you want to use or do use window.location..., ensure that you also add proper handling for a custom basePath
|
||||
|
||||
## TypeScript Best Practices
|
||||
- In TypeScript, if possible, don't use the `any` type
|
||||
- **Use a single params object for functions with multiple arguments** - This makes code more readable at call sites and prevents bugs when arguments of the same type are accidentally swapped:
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - positional arguments are unclear and can be swapped without type errors
|
||||
function sendMessage(userId: string, sessionId: string, projectId: string) {
|
||||
// ...
|
||||
}
|
||||
sendMessage(someString, someOtherString, anotherString); // Which is which?
|
||||
|
||||
// ✅ Good - params object makes intent clear and prevents argument swapping
|
||||
function sendMessage(params: { userId: string; sessionId: string; projectId: string }) {
|
||||
// ...
|
||||
}
|
||||
sendMessage({ userId: someString, sessionId: someOtherString, projectId: anotherString });
|
||||
```
|
||||
|
||||
## General Coding Guidelines
|
||||
- For easier code reviews, prefer not to move functions etc around within a file unless necessary or instructed to do so
|
||||
|
||||
## Development Tips
|
||||
- Before trying to build the package, try running the linter once first
|
||||
+158
-131
@@ -27,7 +27,7 @@ The maintainers are available on [Discord](https://langfuse.com/discord) in case
|
||||
|
||||
## Making a change
|
||||
|
||||
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone. Changes that were not discussed in an issue may be rejected.
|
||||
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone. Large changes that were not discussed in an issue may be rejected.
|
||||
|
||||
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
|
||||
|
||||
@@ -35,10 +35,6 @@ A good first step is to search for open [issues](https://github.com/langfuse/lan
|
||||
|
||||
## Project Overview
|
||||
|
||||
We recommend checking out DeepWiki to familiarize yourself with the project:
|
||||
|
||||
[](https://deepwiki.com/langfuse/langfuse)
|
||||
|
||||
### Technologies we use
|
||||
|
||||
- Application (this repository)
|
||||
@@ -46,7 +42,7 @@ We recommend checking out DeepWiki to familiarize yourself with the project:
|
||||
- NextAuth.js / Auth.js
|
||||
- tRPC: Frontend APIs
|
||||
- Prisma ORM
|
||||
- Zod v4
|
||||
- Zod
|
||||
- Tailwind CSS
|
||||
- shadcn/ui tailwind components (using Radix and tanstack)
|
||||
- Fern: generate OpenAPI spec and Pydantic models
|
||||
@@ -57,35 +53,124 @@ We recommend checking out DeepWiki to familiarize yourself with the project:
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
See this [diagram](https://langfuse.com/self-hosting#architecture) for an overview of the architecture.
|
||||
|
||||
### Network Overview
|
||||
**Langfuse v2**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
User["UI, API, SDKs"]
|
||||
subgraph vpc["VPC"]
|
||||
Web["Web Server<br/>(langfuse/langfuse)"]
|
||||
Worker["Async Worker<br/>(langfuse/worker)"]
|
||||
Postgres["Postgres - OLTP<br/>(Transactional Data)"]
|
||||
Cache["Redis/Valkey<br/>(Cache, Queue)"]
|
||||
Clickhouse["Clickhouse - OLAP<br/>(Observability Data)"]
|
||||
S3["S3 / Blob Storage<br/>(Raw events, multi-modal attachments)"]
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python low-level SDK"]
|
||||
Decorator["observe() decorator"] -->|extends| Python
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
Llamaindex["LlamaIndex Integration"] -->|extends| Python
|
||||
LCPYTHON["Langchain Python Integration"] -->|extends| Python
|
||||
Langflow -->|uses| LCPYTHON
|
||||
LiteLLM -->|uses| Python
|
||||
end
|
||||
subgraph s3["langfuse/langfuse-js"]
|
||||
JS["JS SDK"]
|
||||
LCJS["Langchain JS Integration"] -->|extends| JS
|
||||
Flowise -->|uses| LCJS
|
||||
end
|
||||
end
|
||||
LLM["LLM API/Gateway<br/>(optional)"]
|
||||
|
||||
User --> Web
|
||||
Web --> S3
|
||||
Web --> Postgres
|
||||
Web --> Cache
|
||||
Web --> Clickhouse
|
||||
Web -.->|"optional for playground"| LLM
|
||||
DB[Postgres Database]
|
||||
Redis[Redis]
|
||||
|
||||
Cache --> Worker
|
||||
Worker --> Clickhouse
|
||||
Worker --> Postgres
|
||||
Worker --> S3
|
||||
Worker -.->|"optional for evals"| LLM
|
||||
subgraph s1["Application (langfuse/langfuse/web)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
Prisma[Prisma ORM]
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- Prisma
|
||||
API --- Prisma
|
||||
I --- Prisma
|
||||
end
|
||||
|
||||
Prisma --- DB
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
|
||||
**Langfuse v3 (work in progress, not released yet)**
|
||||
|
||||
> [!NOTE]
|
||||
> Infrastructure will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
|
||||
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python low-level SDK"]
|
||||
Decorator["observe() decorator"] -->|extends| Python
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
Llamaindex["LlamaIndex Integration"] -->|extends| Python
|
||||
LCPYTHON["Langchain Python Integration"] -->|extends| Python
|
||||
Langflow -->|uses| LCPYTHON
|
||||
LiteLLM -->|uses| Python
|
||||
end
|
||||
subgraph s3["langfuse/langfuse-js"]
|
||||
JS["JS SDK"]
|
||||
LCJS["Langchain JS Integration"] -->|extends| JS
|
||||
Flowise -->|uses| LCJS
|
||||
end
|
||||
end
|
||||
|
||||
DB[Postgres Database]
|
||||
Redis[Redis Cache/Queue]
|
||||
Clickhouse[Clickhouse Database]
|
||||
|
||||
subgraph s1["Application (langfuse/langfuse/web)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
ORM
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- ORM
|
||||
API --- ORM
|
||||
I --- ORM
|
||||
end
|
||||
|
||||
subgraph s5["Application (langfuse/langfuse/worker)"]
|
||||
Worker
|
||||
end
|
||||
|
||||
Worker --- DB
|
||||
Worker --- Redis
|
||||
Worker --- Clickhouse
|
||||
|
||||
ORM --- DB
|
||||
ORM --- Redis
|
||||
ORM --- Clickhouse
|
||||
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
|
||||
### Network Overview
|
||||
|
||||
> [!NOTE]
|
||||
> This will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Browser ---|Web UI & TRPC API| App
|
||||
Integrations/SDKs ---|Public HTTP API| App
|
||||
subgraph i1["Application Network"]
|
||||
App["Langfuse Application"]
|
||||
end
|
||||
subgraph i2["Database Network"]
|
||||
DB["Postgres Database"]
|
||||
end
|
||||
App --- DB
|
||||
```
|
||||
|
||||
### Database Overview
|
||||
@@ -94,12 +179,14 @@ The diagram below may not show all relationships if the foreign key is not defin
|
||||
|
||||
Full database schema: [packages/shared/prisma/schema.prisma](packages/shared/prisma/schema.prisma)
|
||||
|
||||
<img src="./packages/shared/prisma/database.svg">
|
||||
|
||||
## Repository Structure
|
||||
|
||||
We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https://turbo.build/repo/docs) to manage the dependencies and build process. The monorepo contains the following packages:
|
||||
|
||||
- `web`: is the main application package providing Frontend and Backend APIs for Langfuse.
|
||||
- `worker`: contains an application for asynchronous processing of tasks.
|
||||
- `worker` (no production yet): contains an application for asynchronous processing of tasks. This package is not yet used in production.
|
||||
- `packages`:
|
||||
- `shared`: contains shared code between the above packages.
|
||||
- `config-eslint`: contains eslint configurations which are shared between the above packages.
|
||||
@@ -110,61 +197,61 @@ We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https:/
|
||||
|
||||
Requirements
|
||||
|
||||
- Node.js 24 as specified in the [.nvmrc](.nvmrc)
|
||||
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
|
||||
- Pnpm v.9.5.0
|
||||
- Docker to run the database locally
|
||||
- Clickhouse client
|
||||
|
||||
**Note:** You can also simply run Langfuse in a **GitHub Codespace** via the provided devcontainer. To do this, click on the green "Code" button in the top right corner of the repository and select "Open with Codespaces".
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Install development dependencies:
|
||||
- [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
|
||||
- [clickhouse binary](https://clickhouse.com/docs/install) on macOS with brew: `brew install --cask clickhouse`
|
||||
|
||||
2. Fork the repository and clone it locally
|
||||
1. Fork the repository and clone it locally
|
||||
2. Run the development database
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langfuse/langfuse.git
|
||||
cd langfuse
|
||||
pnpm run infra:dev:up
|
||||
```
|
||||
|
||||
3. Install dependencies and set up pre-commit hooks
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run prepare # Sets up Husky pre-commit hooks for code formatting
|
||||
```
|
||||
|
||||
4. Create an env file
|
||||
3. Create an env file
|
||||
|
||||
```bash
|
||||
cp .env.dev.example .env
|
||||
```
|
||||
|
||||
5. Run the entire infrastructure in dev mode. **Note**: if you have an existing database, this command wipes it. Also, this will fail on the very first run. Please run it again.
|
||||
4. Install dependencies
|
||||
|
||||
```bash
|
||||
pnpm run dx # first run only (resets db, docker containers, etc...)
|
||||
pnpm run dev # any subsequent runs
|
||||
pnpm install
|
||||
```
|
||||
|
||||
You will be asked whether you want to reset Postgres and ClickHouse. Confirm both with 'Y' and press enter.
|
||||
5. Run the migrations
|
||||
|
||||
6. Open the web app in your browser to start using Langfuse:
|
||||
- [Sign up page, http://localhost:3000](http://localhost:3000)
|
||||
- [Demo project, http://localhost:3000/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a](http://localhost:3000/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a)
|
||||
All database migrations and configs are in the `shared` package.
|
||||
|
||||
7. Log in as a test user:
|
||||
- Username: `demo@langfuse.com`
|
||||
- Password: `password`
|
||||
```bash
|
||||
pnpm --filter=shared run db:migrate
|
||||
|
||||
To get comprehensive example data, you can use the `seed` command:
|
||||
# Optional: seed the database
|
||||
# pnpm run db:seed
|
||||
# pnpm run db:seed:examples
|
||||
# pnpm --filter=shared run db:seed:load
|
||||
```
|
||||
|
||||
```sh
|
||||
pnpm run db:seed:examples
|
||||
```
|
||||
6. Start the development server
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
7. Open the web app in the browser:
|
||||
|
||||
http://localhost:3000
|
||||
|
||||
8. Log in as a test user (after you ran `db:seed` command):
|
||||
|
||||
Username: demo@langfuse.com
|
||||
|
||||
Password: password
|
||||
|
||||
## Monorepo quickstart
|
||||
|
||||
@@ -180,8 +267,6 @@ pnpm run db:seed:examples
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
pnpm --filter=web run dev # execute command only in one package
|
||||
pnpm tc # fast typecheck all packages
|
||||
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
|
||||
```
|
||||
|
||||
In the root `package.json`, you can find scripts which are executed with turbo e.g. `turbo run dev`. These scripts are executed with the help of Turbo. Turbo executes the commands in all packages taking care of the correct order of execution. Task definitions can be found in the `turbo.config.js` file.
|
||||
@@ -217,67 +302,23 @@ pnpm run db:seed:examples
|
||||
|
||||
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
|
||||
|
||||
## Running Unit Tests
|
||||
## Test the public API
|
||||
|
||||
All tests run in the CI and must pass before merging.
|
||||
All tests run against a running langfuse instance and **write/delete real data from the database**.
|
||||
The API is tested using Jest. With the development server running, you can run the tests with:
|
||||
|
||||
### Test Database Setup
|
||||
|
||||
Per default, the tests use the local development database. Therefore, wiping your data in the process.
|
||||
For proper test isolation, create a `.env.test` file in the root directory:
|
||||
Run all
|
||||
|
||||
```bash
|
||||
cp .env.test.example .env.test
|
||||
npm run test
|
||||
```
|
||||
|
||||
Then, a different PostgreSQL and Redis are used for the tests.
|
||||
The `.env.test` file only overrides the set values and falls back on `.env` for all undefined values.
|
||||
Run interactively in watch mode
|
||||
|
||||
- **PostgreSQL**: Uses separate `langfuse_test` database for isolation
|
||||
- **ClickHouse**: Uses shared `default` database for now
|
||||
- **Redis**: Uses database 1 instead of 0 for isolation (Redis data is not cleaned between tests)
|
||||
|
||||
Tests automatically create the PostgreSQL test database if it doesn't exist and clean up data between runs.
|
||||
|
||||
### Tests in the `web` package (public API)
|
||||
|
||||
We're using Jest with in the `web` package. Therefore, if you want to provide an argument to the test runner, do it directly without an intermittent `--`.
|
||||
|
||||
There are three types of unit tests:
|
||||
|
||||
- `test-sync`
|
||||
- `test` (for async folder tests)
|
||||
- `test-client`
|
||||
|
||||
To run a specific test, for example the test: `"should handle special characters in prompt names"` in `prompts.v2.servertest.ts`, run:
|
||||
|
||||
```sh
|
||||
cd web # or with --filter=web
|
||||
pnpm test-sync --testPathPatterns="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
|
||||
# for async folder tests:
|
||||
pnpm test -- --testPathPatterns="observations-api" --testNamePattern="should fetch all observations"
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
To run all tests:
|
||||
|
||||
```sh
|
||||
pnpm run test
|
||||
```
|
||||
|
||||
Run interactively in watch mode (not recommended!)
|
||||
|
||||
```sh
|
||||
pnpm run test:watch
|
||||
```
|
||||
|
||||
### Tests in the `worker` package
|
||||
|
||||
For the `worker` package, we're using `vitest` to run unit tests.
|
||||
|
||||
```sh
|
||||
pnpm run test --filter=worker -- FILE_YOU_WANT_TO_TEST.ts -t "test name"
|
||||
```
|
||||
These tests are also run in CI.
|
||||
|
||||
## CI/CD
|
||||
|
||||
@@ -356,7 +397,7 @@ The background color of the following component will be `hsl(var(--primary))` an
|
||||
| --primary-accent | Primary accent color used for branding | Layout |
|
||||
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
|
||||
| --muted-green | Muted green for Event label | ObservationTree |
|
||||
| --muted-magenta | Muted magenta for Generation label | ObservationTree |
|
||||
| --muted-orange | Muted orange for Generation label | ObservationTree |
|
||||
| --muted-blue | Muted blue for Span label | ObservationTree |
|
||||
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
|
||||
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
|
||||
@@ -398,27 +439,13 @@ You can update the default AI models and prices by adding or updating an entry i
|
||||
Please note that
|
||||
|
||||
- prices are in USD
|
||||
- the list is ordered by ID, so make sure to keep this order and insert new models at the end of the list
|
||||
- the list is ordered by ID, so make sure to keep this order
|
||||
- the `updated_at` field must be updated with the current date in ISO 8601 format. Otherwise, the change will be ignored.
|
||||
|
||||
### Transition period until V3 release
|
||||
|
||||
Until the V3 release, both the JSON record must be updated **and** a migration must be created to continue supporting self-hosted users. Note that the migration must updated both the `models` as well as the `prices` table accordingly.
|
||||
|
||||
## Updating the OpenAPI Specs & fern SDKs
|
||||
|
||||
We maintain the API specifications manually to guarantee a high degree of understandability. If you made changes to the API, please update the respective `.yml` files in `fern/apis/...`.
|
||||
|
||||
To generate the respective `openapi.yml` files which power the online API reference & SDKs, run:
|
||||
|
||||
```sh
|
||||
npx fern-api generate --api server # for the server API
|
||||
npx fern-api generate --api client # for the client API
|
||||
npx fern-api generate --api organizations # for the organizations API
|
||||
```
|
||||
|
||||
**Note:** You need a signed in fern account to run those commands.
|
||||
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Copyright (c) 2023-2025 Langfuse GmbH
|
||||
Copyright (c) 2023--2024 Langfuse GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "ee/", "web/src/ee/", and/or "worker/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Langfuse Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- All content that resides under the "ee/" and/or "web/src/ee" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Finto Technologies Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
-357
@@ -1,357 +0,0 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
<div>
|
||||
<h3>
|
||||
<a href="https://langfuse.com/cn">
|
||||
<strong>🇨🇳 🤝 🪢</strong>
|
||||
</a> ·
|
||||
<a href="https://cloud.langfuse.com">
|
||||
<strong>Langfuse Cloud</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/docs/deployment/self-host">
|
||||
<strong>自托管</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/demo">
|
||||
<strong>演示</strong>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="https://langfuse.com/docs"><strong>文档</strong></a> ·
|
||||
<a href="https://langfuse.com/issues"><strong>报告问题</strong></a> ·
|
||||
<a href="https://langfuse.com/ideas"><strong>功能请求</strong></a> ·
|
||||
<a href="https://langfuse.com/changelog"><strong>更新日志</strong></a> ·
|
||||
<a href="https://langfuse.com/roadmap"><strong>路线图</strong></a> ·
|
||||
</div>
|
||||
<br/>
|
||||
<span>Langfuse 使用 <a href="https://github.com/orgs/langfuse/discussions"><strong>GitHub Discussions</strong></a> 作为支持和功能请求的平台。</span>
|
||||
<br/>
|
||||
<span><b>我们正在招聘。</b> <a href="https://langfuse.com/careers"><strong>加入我们</strong></a>,从事产品工程和技术市场职位。</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-E11311.svg" alt="MIT License">
|
||||
</a>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse">
|
||||
<img src="https://img.shields.io/badge/Y%20Combinator-W23-orange" alt="Y Combinator W23">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/u/langfuse" target="_blank">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langfuse/langfuse?labelColor=%20%23FDB062&logo=Docker&labelColor=%20%23528bff">
|
||||
</a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse">
|
||||
<img src="https://img.shields.io/pypi/dm/langfuse?logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/langfuse">
|
||||
<img src="https://img.shields.io/npm/dm/langfuse?logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package">
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://discord.com/invite/7NXusRtqYU" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1111061815649124414?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="在 Discord 上聊天">
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=langfuse" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/langfuse?logo=X&color=%20%23f5f5f5"
|
||||
alt="在 X (Twitter) 上关注">
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/langfuse/" target="_blank">
|
||||
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||
alt="在 LinkedIn 上关注">
|
||||
</a>
|
||||
<a href="https://github.com/langfuse/langfuse/graphs/commit-activity" target="_blank">
|
||||
<img alt="过去一个月的提交" src="https://img.shields.io/github/commit-activity/m/langfuse/langfuse?labelColor=%20%2332b583&color=%20%2312b76a">
|
||||
</a>
|
||||
<a href="https://github.com/langfuse/langfuse/" target="_blank">
|
||||
<img alt="已关闭的问题" src="https://img.shields.io/github/issues-search?query=repo%3Alangfuse%2Flangfuse%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98">
|
||||
</a>
|
||||
<a href="https://github.com/langfuse/langfuse/discussions/" target="_blank">
|
||||
<img alt="讨论帖数量" src="https://img.shields.io/github/discussions/langfuse/langfuse?labelColor=%20%239b8afb&color=%20%237a5af8">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||
<a href="./README.cn.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
||||
<a href="./README.ja.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
||||
<a href="./README.kr.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发、监控、评估** 以及 **调试** AI 应用。Langfuse 可在几分钟内 **自托管**,并且经过 **实战考验**。
|
||||
|
||||
[](https://langfuse.com/watch-demo)
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||

|
||||
|
||||
- [LLM 应用可观察性](https://langfuse.com/docs/tracing):为你的应用插入仪表代码,并开始将追踪数据传送到 Langfuse,从而追踪 LLM 调用及应用中其他相关逻辑(如检索、嵌入或代理操作)。检查并调试复杂日志及用户会话。试试互动的 [演示](https://langfuse.com/docs/demo) 看看效果。
|
||||
- [提示管理](https://langfuse.com/docs/prompt-management/get-started) 帮助你集中管理、版本控制并协作迭代提示。得益于服务器和客户端的高效缓存,你可以在不增加延迟的情况下反复迭代提示。
|
||||
|
||||
- [评估](https://langfuse.com/docs/evaluation/overview) 是 LLM 应用开发流程的关键组成部分,Langfuse 能够满足你的多样需求。它支持 LLM 作为"裁判"、用户反馈收集、手动标注以及通过 API/SDK 实现自定义评估流程。
|
||||
|
||||
- [数据集](https://langfuse.com/docs/evaluation/dataset-runs/datasets) 为评估你的 LLM 应用提供测试集和基准。它们支持持续改进、部署前测试、结构化实验、灵活评估,并能与 LangChain、LlamaIndex 等框架无缝整合。
|
||||
|
||||
- [LLM 试玩平台](https://langfuse.com/docs/playground) 是用于测试和迭代提示及模型配置的工具,缩短反馈周期,加速开发。当你在追踪中发现异常结果时,可以直接跳转至试玩平台进行调整。
|
||||
|
||||
- [综合 API](https://langfuse.com/docs/api):Langfuse 常用于驱动定制化的 LLMOps 工作流程,同时利用 Langfuse 提供的构建模块和 API。我们提供 OpenAPI 规格、Postman 集合以及针对 Python 和 JS/TS 的类型化 SDK。
|
||||
|
||||
## 📦 部署 Langfuse
|
||||
|
||||

|
||||
|
||||
### Langfuse Cloud
|
||||
|
||||
由 Langfuse 团队管理的部署,提供慷慨的免费额度(爱好者计划),无需信用卡。
|
||||
|
||||
<div align="center">
|
||||
<a href="https://cloud.langfuse.com" target="_blank">
|
||||
<img alt="注册 Langfuse Cloud" src="https://img.shields.io/badge/»%20Sign%20up%20for%20Langfuse%20Cloud-8A2BE2?&color=orange">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 自托管 Langfuse
|
||||
|
||||
在你自己的基础设施上运行 Langfuse:
|
||||
|
||||
- [本地(docker compose)](https://langfuse.com/self-hosting/local):使用 Docker Compose 在你的机器上于 5 分钟内运行 Langfuse。
|
||||
|
||||
```bash:README.md/docker-compose
|
||||
# 获取最新的 Langfuse 仓库副本
|
||||
git clone https://github.com/langfuse/langfuse.git
|
||||
cd langfuse
|
||||
|
||||
# 运行 Langfuse 的 docker compose
|
||||
docker compose up
|
||||
```
|
||||
|
||||
- [Kubernetes(Helm)](https://langfuse.com/self-hosting/kubernetes-helm):使用 Helm 在 Kubernetes 集群上部署 Langfuse。这是推荐的生产环境部署方式。
|
||||
|
||||
- [虚拟机](https://langfuse.com/self-hosting/docker-compose):使用 Docker Compose 在单台虚拟机上部署 Langfuse。
|
||||
|
||||
- Terraform 模板: [AWS](https://langfuse.com/self-hosting/aws)、[Azure](https://langfuse.com/self-hosting/azure)、[GCP](https://langfuse.com/self-hosting/gcp)
|
||||
|
||||
请参阅 [自托管文档](https://langfuse.com/self-hosting) 了解更多关于架构和配置选项的信息。
|
||||
|
||||
## 🔌 集成
|
||||
|
||||

|
||||
|
||||
### 主要集成:
|
||||
|
||||
| 集成 | 支持语言/平台 | 描述 |
|
||||
| ------------------------------------------------------------------------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | 使用 SDK 进行手动仪表化,实现全面灵活性。 |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | 通过直接替换 OpenAI SDK 实现自动仪表化。 |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | 通过传入回调处理器至 Langchain 应用实现自动仪表化。 |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | 通过 LlamaIndex 回调系统实现自动仪表化。 |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | 通过 Haystack 内容追踪系统实现自动仪表化。 |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (仅代理) | 允许使用任何 LLM 替代 GPT。支持 Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate(100+ LLMs)。 |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | 基于 TypeScript 的工具包,帮助开发者使用 React、Next.js、Vue、Svelte 和 Node.js 构建 AI 驱动的应用。 |
|
||||
| [API](https://langfuse.com/docs/api) | | 直接调用公共 API。提供 OpenAPI 规格。 |
|
||||
| [Google VertexAI 和 Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 模型 | 在 Google 上运行基础模型和微调模型。 |
|
||||
|
||||
### 与 Langfuse 集成的软件包:
|
||||
|
||||
| 名称 | 类型 | 描述 |
|
||||
| ----------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 库 | 用于获取结构化 LLM 输出(JSON、Pydantic)的库。 |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 库 | 一个系统性优化语言模型提示和权重的框架。 |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 模型 | 在 AWS 上运行基础模型和微调模型。 |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 库 | 构建 LLM 应用的 Python 工具包。 |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 模型(本地) | 在你的机器上轻松运行开源 LLM。 |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 代理框架 | 用于构建分布式代理的开源 LLM 平台。 |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 聊天/代理界面 | 基于 JS/TS 的无代码构建器,用于定制化 LLM 流程。 |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 聊天/代理界面 | 基于 Python 的 LangChain 用户界面,采用 react-flow 设计,提供便捷的实验与原型构建体验。 |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | 聊天/代理界面 | 带有无代码构建器的开源 LLM 应用开发平台。 |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 聊天/代理界面 | 自托管的 LLM 聊天网页界面,支持包括自托管和本地模型在内的多种 LLM 运行器。 |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 工具 | 开源 LLM 测试平台。 |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 聊天/代理界面 | 开源聊天机器人平台。 |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 平台 | 开源语音 AI 平台。 |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 代理 | 构建分布式代理的开源 LLM 平台。 |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 聊天/代理界面 | 开源 Python 库,可用于构建类似聊天 UI 的网页界面。 |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | 代理 | 构建分布式代理的开源 LLM 平台。 |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 代理 | 开源 AI 代理框架。 |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 代理 | 多代理框架,用于实现代理之间的协作与工具调用。 |
|
||||
|
||||
## 🚀 快速入门
|
||||
|
||||
为你的应用增加仪表代码,并开始将追踪数据上传到 Langfuse,从而记录 LLM 调用及应用中其他相关逻辑(如检索、嵌入或代理操作)。
|
||||
|
||||
### 1️⃣ 创建新项目
|
||||
|
||||
1. [创建 Langfuse 账户](https://cloud.langfuse.com/auth/sign-up) 或 [自托管](https://langfuse.com/self-hosting)
|
||||
2. 创建一个新项目
|
||||
3. 在项目设置中创建新的 API 凭证
|
||||
|
||||
### 2️⃣ 记录你的第一个 LLM 调用
|
||||
|
||||
使用 [<code>@observe()</code> 装饰器](https://langfuse.com/docs/sdk/python/decorators) 可轻松跟踪任何 Python LLM 应用。在本快速入门中,我们还使用了 Langfuse 的 [OpenAI 集成](https://langfuse.com/integrations/model-providers/openai-py) 来自动捕获所有模型参数。
|
||||
|
||||
> [!提示]
|
||||
> 不使用 OpenAI?请访问 [我们的文档](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse) 了解如何记录其他模型和框架。
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pip install langfuse openai
|
||||
```
|
||||
|
||||
配置环境变量(创建名为 **.env** 的文件):
|
||||
|
||||
```bash:.env
|
||||
LANGFUSE_SECRET_KEY="sk-lf-..."
|
||||
LANGFUSE_PUBLIC_KEY="pk-lf-..."
|
||||
LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 欧盟区域
|
||||
# LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" # 🇺🇸 美洲区域
|
||||
```
|
||||
|
||||
创建示例代码(文件名:**main.py**):
|
||||
|
||||
```python:main.py
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI 集成
|
||||
|
||||
@observe()
|
||||
def story():
|
||||
return openai.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=[{"role": "user", "content": "What is Langfuse?"}],
|
||||
).choices[0].message.content
|
||||
|
||||
@observe()
|
||||
def main():
|
||||
return story()
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
### 3️⃣ 在 Langfuse 中查看追踪记录
|
||||
|
||||
在 Langfuse 中查看你的语言模型调用及其他应用逻辑。
|
||||
|
||||

|
||||
|
||||
_[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/2cec01e3-3dc2-472f-afcf-3b968cf0c1f4?timestamp=2025-02-10T14%3A27%3A30.275Z&observation=cb5ff844-07ef-41e6-b8e2-6c64344bc13b)_
|
||||
|
||||
> [!提示]
|
||||
>
|
||||
> [了解更多](https://langfuse.com/docs/tracing) 关于 Langfuse 中的追踪,或试试 [互动演示](https://langfuse.com/docs/demo)。
|
||||
|
||||
## ⭐️ 给我们加星
|
||||
|
||||

|
||||
|
||||
## 💭 支持
|
||||
|
||||
查找问题答案:
|
||||
|
||||
- 我们的 [文档](https://langfuse.com/docs) 是查找答案的最佳起点。内容全面,我们投入大量时间进行维护。你也可以通过 GitHub 提出文档修改建议。
|
||||
- [Langfuse 常见问题](https://langfuse.com/faq) 解答了最常见的问题。
|
||||
- 使用 "Ask AI" 立即获取问题答案。
|
||||
|
||||
支持渠道:
|
||||
|
||||
- **在 GitHub Discussions 的 [公共问答](https://github.com/orgs/langfuse/discussions/categories/support) 中提出任何问题。** 请尽量提供详细信息(如代码片段、截图、背景信息)以帮助我们理解你的问题。
|
||||
- 在 GitHub Discussions 中 [提出功能请求](https://github.com/orgs/langfuse/discussions/categories/ideas)。
|
||||
- 在 GitHub Issues 中 [报告 Bug](https://github.com/langfuse/langfuse/issues)。
|
||||
- 对于时效性较强的问题,请通过应用内聊天小部件联系我们。
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎你的贡献!
|
||||
|
||||
- 在 GitHub Discussions 中为 [想法](https://github.com/orgs/langfuse/discussions/categories/ideas)投票。
|
||||
- 提出并评论 [问题](https://github.com/langfuse/langfuse/issues)。
|
||||
- 提交 PR —— 详情请参见 [CONTRIBUTING.md](CONTRIBUTING.md),了解如何搭建开发环境。
|
||||
|
||||
## 🥇 许可证
|
||||
|
||||
除 `ee` 文件夹外,本仓库采用 MIT 许可证。详情请参见 [LICENSE](LICENSE) 以及 [文档](https://langfuse.com/docs/open-source)。
|
||||
|
||||
## ⭐️ 星标历史
|
||||
|
||||
<a href="https://star-history.com/#langfuse/langfuse&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
|
||||
<img alt="星标历史图表" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" style="border-radius: 15px;" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## ❤️ 使用 Langfuse 的开源项目
|
||||
|
||||
以下是使用 Langfuse 的顶级开源 Python 项目,按星标数排名([来源](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
|
||||
|
||||
| 仓库 | 星数 |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 安全与隐私
|
||||
|
||||
我们非常重视数据安全和隐私。更多信息请参阅我们的 [安全与隐私](https://langfuse.com/security) 页面。
|
||||
|
||||
### 遥测
|
||||
|
||||
默认情况下,Langfuse 会自动将自托管实例的基础使用统计数据上传至集中服务器(PostHog)。
|
||||
|
||||
这有助于我们:
|
||||
|
||||
1. 了解 Langfuse 的使用情况,并改进最关键的功能。
|
||||
2. 跟踪整体使用数据,以便内部及外部(例如筹款)报告。
|
||||
|
||||
所有数据均不会与第三方共享,也不包含任何敏感信息。我们对这一过程保持高度透明,你可以在 [此处](/web/src/features/telemetry/index.ts) 查看我们收集的具体数据。
|
||||
|
||||
你可以通过设置 `TELEMETRY_ENABLED=false` 来选择退出。
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
-359
@@ -1,359 +0,0 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
<div>
|
||||
<h3>
|
||||
<a href="https://langfuse.com/jp">
|
||||
<strong>🇯🇵 🤝 🪢</strong>
|
||||
</a> ·
|
||||
<a href="https://cloud.langfuse.com">
|
||||
<strong>Langfuse Cloud</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/docs/deployment/self-host">
|
||||
<strong>セルフホスティング</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/demo">
|
||||
<strong>デモ</strong>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="https://langfuse.com/docs"><strong>ドキュメント</strong></a> ·
|
||||
<a href="https://langfuse.com/issues"><strong>バグ報告</strong></a> ·
|
||||
<a href="https://langfuse.com/ideas"><strong>機能リクエスト</strong></a> ·
|
||||
<a href="https://langfuse.com/changelog"><strong>変更履歴</strong></a> ·
|
||||
<a href="https://langfuse.com/roadmap"><strong>ロードマップ</strong></a> ·
|
||||
</div>
|
||||
<br/>
|
||||
<span>Langfuseは、サポートと機能リクエストのために <a href="https://github.com/orgs/langfuse/discussions"><strong>GitHub Discussions</strong></a> を利用しています。</span>
|
||||
<br/>
|
||||
<span><b>We're hiring.</b> <a href="https://langfuse.com/careers"><strong>チームに加わる</strong></a> (製品エンジニアリングおよびテクニカルGTMのポジション)への応募をお待ちしています。</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-E11311.svg" alt="MIT License">
|
||||
</a>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange" alt="Y Combinator W23"></a>
|
||||
<a href="https://hub.docker.com/u/langfuse" target="_blank">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langfuse/langfuse?labelColor=%20%23FDB062&logo=Docker&labelColor=%20%23528bff"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
|
||||
<br/>
|
||||
<a href="https://discord.com/invite/7NXusRtqYU" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1111061815649124414?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="chat on Discord"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=langfuse" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/langfuse?logo=X&color=%20%23f5f5f5"
|
||||
alt="follow on X(Twitter)"></a>
|
||||
<a href="https://www.linkedin.com/company/langfuse/" target="_blank">
|
||||
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||
alt="follow on LinkedIn"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/graphs/commit-activity" target="_blank">
|
||||
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langfuse/langfuse?labelColor=%20%2332b583&color=%20%2312b76a"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/" target="_blank">
|
||||
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alangfuse%2Flangfuse%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/discussions/" target="_blank">
|
||||
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langfuse/langfuse?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||
<a href="./README.cn.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
||||
<a href="./README.ja.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
||||
<a href="./README.kr.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
Langfuseは**オープンソースのLLMエンジニアリング**プラットフォームです。
|
||||
チームが共同でAIアプリケーションを**開発、監視、評価**、および**デバッグ**するのを支援します。
|
||||
Langfuseは**数分でセルフホスト可能**で、**多くの実績を持つ**システムです。
|
||||
|
||||
[](https://langfuse.com/watch-demo)
|
||||
|
||||
## ✨ コア機能
|
||||
|
||||

|
||||
|
||||
- **[LLMアプリケーションの可観測性](https://langfuse.com/docs/tracing):**
|
||||
アプリケーションにインストゥルメンテーションを導入し、Langfuseへトレースを取り込むことで、LLM呼び出しやリトリーバル、埋め込み、エージェントアクションなどの関連ロジックを追跡できます。
|
||||
複雑なログやユーザーセッションを解析・デバッグできます。
|
||||
インタラクティブな[デモ](https://langfuse.com/docs/demo)で動作を確認してください。
|
||||
|
||||
- **[プロンプト管理](https://langfuse.com/docs/prompt-management/get-started):**
|
||||
プロンプトを一元管理し、バージョン管理しながら共同で改善を行えます。
|
||||
サーバーおよびクライアント側で強力なキャッシングを行うため、アプリケーションのレイテンシを増やすことなくプロンプトの改良が可能です。
|
||||
|
||||
- **[評価](https://langfuse.com/docs/evaluation/overview):**
|
||||
評価はLLMアプリケーション開発ワークフローの要であり、Langfuseは多様なニーズに対応します。
|
||||
LLMを判定者として用いる方法、ユーザーフィードバックの収集、手動によるラベリング、API/SDKを通じたカスタム評価パイプラインをサポートします。
|
||||
|
||||
- **[データセット](https://langfuse.com/docs/evaluation/dataset-runs/datasets):**
|
||||
LLMアプリケーション評価用のテストセットやベンチマークを構築できます。
|
||||
継続的な改善、事前デプロイテスト、構造化された実験、柔軟な評価、さらにLangChainやLlamaIndexなどとのシームレスな統合をサポートします。
|
||||
|
||||
- **[LLMプレイグラウンド](https://langfuse.com/docs/playground):**
|
||||
プロンプトやモデル設定のテスト・反復作業を支援するツールで、フィードバックループを短縮し開発を加速します。
|
||||
トレースで不具合が見つかった場合、直接プレイグラウンドへ飛び、迅速に改善できます。
|
||||
|
||||
- **[包括的なAPI](https://langfuse.com/docs/api):**
|
||||
LangfuseはAPIを通じて提供されるビルディングブロックを用い、カスタムLLMOpsワークフローの基盤として頻繁に利用されます。
|
||||
OpenAPI仕様、Postmanコレクション、PythonやJS/TS向けの型付きSDKが利用可能です。
|
||||
|
||||
## 📦 Langfuseのデプロイ
|
||||
|
||||

|
||||
|
||||
### Langfuse Cloud
|
||||
|
||||
Langfuseチームによるマネージドデプロイメント。充実した無料プラン(ホビープラン)で、クレジットカード不要です。
|
||||
|
||||
<div align="center">
|
||||
<a href="https://cloud.langfuse.com" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/»%20Sign%20up%20for%20Langfuse%20Cloud-8A2BE2?&color=orange">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### セルフホスティング Langfuse
|
||||
|
||||
自身のインフラ上でLangfuseを実行できます:
|
||||
|
||||
- **[Local (docker compose)](https://langfuse.com/self-hosting/local):**
|
||||
Docker Composeを使用して、たった5分で自分のマシン上でLangfuseを実行できます.
|
||||
|
||||
```bash
|
||||
# 最新のLangfuseリポジトリのコピーを取得
|
||||
git clone https://github.com/langfuse/langfuse.git
|
||||
cd langfuse
|
||||
|
||||
# Langfuseのdocker composeを起動
|
||||
docker compose up
|
||||
```
|
||||
|
||||
- **[Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm):**
|
||||
Helmを使用してKubernetesクラスター上でLangfuseを実行します。
|
||||
こちらが推奨される本番環境でのデプロイ方法です。
|
||||
|
||||
- **[VM](https://langfuse.com/self-hosting/docker-compose):**
|
||||
Docker Composeを使用して、単一の仮想マシン上でLangfuseを実行します。
|
||||
|
||||
- Terraform テンプレート: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
|
||||
|
||||
[セルフホスティングのドキュメント](https://langfuse.com/self-hosting)を参照し、アーキテクチャや設定オプションの詳細をご確認ください。
|
||||
|
||||
## 🔌 インテグレーション
|
||||
|
||||

|
||||
|
||||
### 主なインテグレーション:
|
||||
|
||||
| インテグレーション | 対応言語・環境 | 説明 |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDKを利用して手動でインストゥルメンテーションを実装し、完全な柔軟性を提供します。 |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | OpenAI SDKのドロップイン置換による自動インストゥルメンテーションを実現します。 |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchainアプリケーションにコールバックハンドラーを渡すことで自動的に計測します。 |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndexのコールバックシステムを介して自動的にインストゥルメントします。 |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystackのコンテンツトレースシステムを利用した自動インストゥルメンテーションを実現します。 |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPTのドロップイン置換として任意のLLMを使用できます。Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate(100+ LLM)に対応。 |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React、Next.js、Vue、Svelte、Node.jsを使用してAI搭載アプリケーションの構築を支援するTypeScriptツールキットです。 |
|
||||
| [API](https://langfuse.com/docs/api) | | 公開APIを直接呼び出すことが可能です。OpenAPI仕様も利用できます。 |
|
||||
|
||||
### Langfuseと統合されているパッケージ:
|
||||
|
||||
| 名前 | タイプ | 説明 |
|
||||
| ------------------------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | ライブラリ | 構造化されたLLM出力(JSON、Pydantic)を取得するためのライブラリ |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | ライブラリ | LLMプロンプトや重み付けを体系的に最適化するためのフレームワーク |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | ライブラリ | LLMアプリケーション構築用のPythonツールキット |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | モデル(ローカル) | オープンソースLLMを手軽にローカルで実行するためのツール |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | モデル | AWS上でファウンデーションモデルやファインチューニング済みモデルを実行 |
|
||||
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | モデル | Google上でファウンデーションモデルやファインチューニング済みモデルを実行 |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | エージェントフレームワーク | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | チャット/エージェント UI | JS/TSのノーコードビルダーで、カスタマイズ可能なLLMフローを構築 |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | チャット/エージェント UI | PythonベースのUIで、react-flowを用いてLangChainの実験やプロトタイピングを容易に実現 |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | チャット/エージェント UI | ノーコードでLLMアプリ開発が可能なオープンソースプラットフォーム |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | チャット/エージェント UI | 自前ホストおよびローカルモデルに対応するLLMチャットWeb UI |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | ツール | オープンソースのLLMテストプラットフォーム |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | チャット/エージェント UI | オープンソースのチャットボットプラットフォーム |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | プラットフォーム | オープンソースの音声AIプラットフォーム |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | チャット/エージェント UI | チャットUIなどのWebインターフェース構築のためのオープンソースPythonライブラリ |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | エージェント | オープンソースのAIエージェントフレームワーク |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | エージェント | エージェントの協調とツール利用を実現するマルチエージェントフレームワーク |
|
||||
|
||||
## 🚀 クイックスタート
|
||||
|
||||
アプリケーションにインストゥルメンテーションを導入し、LLM呼び出しやリトリーバル、埋め込み、エージェントアクションなどの動作をLangfuseに記録しましょう。
|
||||
複雑なログやユーザーセッションの解析・デバッグが可能になります。
|
||||
|
||||
### 1️⃣ 新規プロジェクトの作成
|
||||
|
||||
1. [Langfuseアカウント作成](https://cloud.langfuse.com/auth/sign-up) または [セルフホスト](https://langfuse.com/self-hosting)
|
||||
2. 新規プロジェクトを作成
|
||||
3. プロジェクト設定で新しいAPIクレデンシャルを作成
|
||||
|
||||
### 2️⃣ 初めてのLLM呼び出しのログ記録
|
||||
|
||||
[`@observe()` デコレーター](https://langfuse.com/docs/sdk/python/decorators)を利用することで、任意のPython製LLMアプリケーションのトレースが簡単に行えます。
|
||||
このクイックスタートでは、Langfuseの[OpenAI統合](https://langfuse.com/integrations/model-providers/openai-py)を使用して、全てのモデルパラメータを自動で取得します。
|
||||
|
||||
> [!TIP]
|
||||
> OpenAIを利用していない場合は、[こちらのドキュメント](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse)で、他のモデルやフレームワークのログ記録方法をご確認ください。
|
||||
|
||||
```bash
|
||||
pip install langfuse openai
|
||||
```
|
||||
|
||||
```bash filename=".env"
|
||||
LANGFUSE_SECRET_KEY="sk-lf-..."
|
||||
LANGFUSE_PUBLIC_KEY="pk-lf-..."
|
||||
LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 EUリージョン
|
||||
# LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" # 🇺🇸 USリージョン
|
||||
```
|
||||
|
||||
```python:/@observe()/ /from langfuse.openai import openai/ filename="main.py"
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI統合
|
||||
|
||||
@observe()
|
||||
def story():
|
||||
return openai.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=[{"role": "user", "content": "What is Langfuse?"}],
|
||||
).choices[0].message.content
|
||||
|
||||
@observe()
|
||||
def main():
|
||||
return story()
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
### 3️⃣ Langfuseでトレースを確認する
|
||||
|
||||
Langfuse上で、LLM呼び出しおよびその他のアプリケーションロジックのトレースを確認できます。
|
||||
|
||||

|
||||
|
||||
_[Langfuseの公開トレース例](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/2cec01e3-3dc2-472f-afcf-3b968cf0c1f4?timestamp=2025-02-10T14%3A27%3A30.275Z&observation=cb5ff844-07ef-41e6-b8e2-6c64344bc13b)_
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Langfuseでのトレースの詳細については、[こちら](https://langfuse.com/docs/tracing)をご参照いただくか、[インタラクティブデモ](https://langfuse.com/docs/demo)でお試しください。
|
||||
|
||||
## ⭐️ Star Langfuse
|
||||
|
||||

|
||||
|
||||
## 💭 サポート
|
||||
|
||||
質問の回答をお探しの場合は:
|
||||
|
||||
- 当社の[ドキュメント](https://langfuse.com/docs)は、回答を探すための最良の出発点です。内容が充実しており、継続的なメンテナンスに努めています。GitHubを通じてドキュメントへの修正提案も可能です。
|
||||
- よくある質問は[Langfuse FAQ](https://langfuse.com/faq)にまとめられています。
|
||||
- [Ask AI](https://langfuse.com/docs/ask-ai)を利用すれば、質問に対して即座に回答を得ることができます。
|
||||
- 日本語のサポートや決済, 請求書払いなどをお求めの場合は、日本のリセラー (https://gao-ai.com) にご相談ください。
|
||||
|
||||
サポートチャネル:
|
||||
|
||||
- **GitHub Discussionsの[パブリックQ&A](https://github.com/orgs/langfuse/discussions/categories/support)で質問してください。**
|
||||
質問には、コードスニペット、スクリーンショット、背景情報など、できるだけ詳細な情報を含めるとスムーズな対応が可能です。
|
||||
- GitHub Discussionsで[機能リクエスト](https://github.com/orgs/langfuse/discussions/categories/ideas)を投稿してください。
|
||||
- GitHub Issuesにて[バグ報告](https://github.com/langfuse/langfuse/issues)を行ってください。
|
||||
- 緊急の問い合わせの場合は、アプリ内チャットウィジェットでご連絡ください。
|
||||
|
||||
## 🤝 貢献
|
||||
|
||||
皆様からの貢献を歓迎します!
|
||||
|
||||
- GitHub Discussionsの[アイデア](https://github.com/orgs/langfuse/discussions/categories/ideas)に投票してください。
|
||||
- [Issues](https://github.com/langfuse/langfuse/issues)を作成・コメントしてください。
|
||||
- プルリクエストを送信してください。開発環境のセットアップ方法については[CONTRIBUTING.md](CONTRIBUTING.md)をご参照ください。
|
||||
|
||||
## 🥇 ライセンス
|
||||
|
||||
このリポジトリは、`ee`フォルダを除き、MITライセンスの下で公開されています。
|
||||
詳細は[LICENSE](LICENSE)および[オープンソースに関するドキュメント](https://langfuse.com/docs/open-source)をご確認ください。
|
||||
|
||||
## ⭐️ スターの履歴
|
||||
|
||||
<a href="https://star-history.com/#langfuse/langfuse&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" style="border-radius: 15px;" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## ❤️ Langfuseを利用しているオープンソースプロジェクト
|
||||
|
||||
Langfuseを利用している主要なオープンソースPythonプロジェクト(スター数順): ([出典](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents))
|
||||
|
||||
| リポジトリ | スター |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 セキュリティとプライバシー
|
||||
|
||||
データのセキュリティとプライバシーは非常に重要です。
|
||||
詳細につきましては、[セキュリティとプライバシー](https://langfuse.com/security)ページをご参照ください。
|
||||
|
||||
### テレメトリー
|
||||
|
||||
デフォルトでは、Langfuseは以下の目的でセルフホストされたインスタンスの基本的な使用統計情報を中央サーバ(PostHog)へ自動的に報告します。
|
||||
|
||||
1. Langfuseの利用状況を把握し、最も重要な機能の改善に役立てる
|
||||
2. 内部および外部(例:資金調達)のレポートのために全体の利用状況を追跡する
|
||||
|
||||
収集されたデータは第三者と共有されず、機微な情報は一切含まれていません。
|
||||
当社はこの点について極力透明性を保っており、収集される具体的なデータの詳細は[こちら](/web/src/features/telemetry/index.ts)で確認できます。
|
||||
|
||||
`TELEMETRY_ENABLED=false` を設定することで、テレメトリーの報告をオプトアウトできます.
|
||||
-341
@@ -1,341 +0,0 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
<div>
|
||||
<h3>
|
||||
<a href="https://langfuse.com/kr">
|
||||
<strong>🇰🇷 🤝 🪢</strong>
|
||||
</a> ·
|
||||
<a href="https://cloud.langfuse.com">
|
||||
<strong>Langfuse Cloud</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/docs/deployment/self-host">
|
||||
<strong>셀프 호스트</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/demo">
|
||||
<strong>데모</strong>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="https://langfuse.com/docs"><strong>문서</strong></a> ·
|
||||
<a href="https://langfuse.com/issues"><strong>버그 신고</strong></a> ·
|
||||
<a href="https://langfuse.com/ideas"><strong>기능 요청</strong></a> ·
|
||||
<a href="https://langfuse.com/changelog"><strong>변경 내역</strong></a> ·
|
||||
<a href="https://langfuse.com/roadmap"><strong>로드맵</strong></a> ·
|
||||
</div>
|
||||
<br/>
|
||||
<span>Langfuse는 지원 및 기능 요청을 위해 <a href="https://github.com/orgs/langfuse/discussions"><strong>GitHub Discussions</strong></a>를 사용합니다.</span>
|
||||
<br/>
|
||||
<span><b>채용 중입니다.</b> <a href="https://langfuse.com/careers"><strong>함께 하세요</strong></a> – 제품 엔지니어링 및 기술 go-to-market 역할의 인재를 찾고 있습니다.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-E11311.svg" alt="MIT License">
|
||||
</a>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange" alt="Y Combinator W23"></a>
|
||||
<a href="https://hub.docker.com/u/langfuse" target="_blank">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langfuse/langfuse?labelColor=%20%23FDB062&logo=Docker&labelColor=%20%23528bff"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
|
||||
<br/>
|
||||
<a href="https://discord.com/invite/7NXusRtqYU" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1111061815649124414?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="chat on Discord"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=langfuse" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/langfuse?logo=X&color=%20%23f5f5f5"
|
||||
alt="follow on X(Twitter)"></a>
|
||||
<a href="https://www.linkedin.com/company/langfuse/" target="_blank">
|
||||
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||
alt="follow on LinkedIn"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/graphs/commit-activity" target="_blank">
|
||||
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langfuse/langfuse?labelColor=%20%2332b583&color=%20%2312b76a"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/" target="_blank">
|
||||
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alangfuse%2Flangfuse%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/discussions/" target="_blank">
|
||||
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langfuse/langfuse?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||
<a href="./README.cn.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
||||
<a href="./README.ja.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
||||
<a href="./README.kr.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
Langfuse는 **오픈 소스 LLM 엔지니어링** 플랫폼입니다.
|
||||
팀이 협업하여 AI 애플리케이션을 **개발, 모니터링, 평가** 및 **디버그**할 수 있도록 도와줍니다.
|
||||
Langfuse는 몇 분 안에 셀프 호스팅할 수 있으며, 검증된(battle-tested) 솔루션입니다.
|
||||
|
||||
[](https://langfuse.com/watch-demo)
|
||||
|
||||
## ✨ 주요 기능
|
||||
|
||||

|
||||
|
||||
- **LLM 애플리케이션 관측**
|
||||
앱에 계측(instrumentation)을 추가하여 Langfuse로 trace 데이터를 수집함으로써, 검색, 임베딩, 또는 에이전트 동작과 같은 LLM 호출 및 기타 관련 로직을 추적할 수 있습니다. 복잡한 로그와 사용자 세션을 확인 및 디버깅 해보세요. 인터랙티브 데모를 통해 실제 작동 예를 확인할 수 있습니다.
|
||||
|
||||
- **프롬프트 관리**
|
||||
프롬프트를 중앙에서 관리하고 버전 관리하며 협업으로 수정할 수 있도록 도와줍니다. 서버와 클라이언트 측의 강력한 캐싱 덕분에 애플리케이션에 지연(latency)을 추가하지 않고도 프롬프트를 반복 개선할 수 있습니다.
|
||||
|
||||
- **평가**
|
||||
LLM 애플리케이션 개발 워크플로우에서 핵심적인 역할을 하며, Langfuse는 여러분의 필요에 맞게 유연하게 대응합니다. LLM을 심사자로 활용하는 기능, 사용자 피드백 수집, 수동 라벨링 및 API/SDK를 통한 맞춤 평가 파이프라인을 지원합니다.
|
||||
|
||||
- **데이터셋**
|
||||
LLM 애플리케이션 평가를 위한 테스트 세트와 벤치마크를 제공하여, 지속적인 개선, 배포 전 테스트, 구조화된 실험, 유연한 평가 및 LangChain과 LlamaIndex와 같은 프레임워크와의 원활한 통합을 지원합니다.
|
||||
|
||||
- **LLM 플레이그라운드**
|
||||
프롬프트와 모델 구성에 대해 테스트 및 반복 개선할 수 있는 도구로, 피드백 루프를 단축하여 개발 속도를 높여줍니다. trace에서 이상한 결과가 발생하면 플레이그라운드로 바로 이동해 개선할 수 있습니다.
|
||||
|
||||
- **종합 API**
|
||||
Langfuse는 API를 통해 제공되는 구성 요소들을 활용하여 맞춤형 LLMOps 워크플로우를 강화하는 데 자주 사용됩니다. OpenAPI 명세, Postman 컬렉션, Python 및 JS/TS용 타입드 SDK가 제공됩니다.
|
||||
|
||||
## 📦 Langfuse 배포
|
||||
|
||||

|
||||
|
||||
### Langfuse Cloud
|
||||
|
||||
Langfuse 팀이 관리하는 배포 방식으로, 후한 무료 플랜(취미 플랜)을 제공하며 신용카드가 필요하지 않습니다.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://cloud.langfuse.com" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/»%20Sign%20up%20for%20Langfuse%20Cloud-8A2BE2?&color=orange">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Langfuse 셀프 호스트
|
||||
|
||||
자체 인프라에서 Langfuse를 실행하세요:
|
||||
|
||||
- [로컬 (docker compose)](https://langfuse.com/self-hosting/local): Docker Compose를 사용하여 본인의 컴퓨터에서 5분 안에 Langfuse를 실행할 수 있습니다.
|
||||
|
||||
```bash
|
||||
# 최신 Langfuse 저장소 클론
|
||||
git clone https://github.com/langfuse/langfuse.git
|
||||
cd langfuse
|
||||
|
||||
# langfuse docker compose 실행
|
||||
docker compose up
|
||||
```
|
||||
|
||||
- [Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm): Helm을 사용해 Kubernetes 클러스터에서 Langfuse를 실행합니다. 이는 권장되는 프로덕션 배포 방식입니다.
|
||||
- [VM](https://langfuse.com/self-hosting/docker-compose): Docker Compose를 사용해 단일 가상 머신에서 Langfuse를 실행합니다.
|
||||
- Terraform 템플릿: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
|
||||
|
||||
자세한 내용은 [자체 호스팅 문서](https://langfuse.com/self-hosting)를 참조하세요.
|
||||
|
||||
## 🔌 통합 기능
|
||||
|
||||

|
||||
|
||||
### 주요 통합:
|
||||
|
||||
| 통합 | 지원 | 설명 |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDK를 사용하여 완전한 유연성을 갖춘 수동 계측(manual instrumentation)을 수행합니다. |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | OpenAI SDK의 드롭인 대체(drop-in replacement)를 통해 자동 계측(automated instrumentation)을 수행합니다. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchain 애플리케이션에 callback 핸들러를 전달하여 자동 계측합니다. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndex 콜백 시스템을 통한 자동 계측을 지원합니다. |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystack 콘텐츠 추적 시스템을 통한 자동 계측을 지원합니다. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPT의 드롭인 대체품으로 어떤 LLM도 사용할 수 있습니다. Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate 등 100개 이상의 LLM 지원. |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React, Next.js, Vue, Svelte, Node.js와 함께 AI 기반 애플리케이션 구축을 돕는 TypeScript 툴킷입니다. |
|
||||
| [API](https://langfuse.com/docs/api) | | 공개 API를 직접 호출합니다. OpenAPI 명세가 제공됩니다. |
|
||||
|
||||
### Langfuse와 통합된 패키지:
|
||||
|
||||
| 이름 | 유형 | 설명 |
|
||||
| ------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 라이브러리 | 구조화된 LLM 출력을(JSON, Pydantic) 얻기 위한 라이브러리입니다. |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 라이브러리 | 언어 모델 프롬프트와 가중치를 체계적으로 최적화하는 프레임워크입니다. |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 라이브러리 | LLM 애플리케이션 구축을 위한 Python 툴킷입니다. |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 모델 (로컬) | 자신의 컴퓨터에서 오픈 소스 LLM을 손쉽게 실행할 수 있습니다. |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 모델 | AWS에서 기본 및 파인튜닝된 모델을 실행합니다. |
|
||||
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 모델 | Google에서 기본 및 파인튜닝된 모델을 실행합니다. |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 에이전트 프레임워크 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 채팅/에이전트 UI | 맞춤형 LLM 플로우를 위한 JS/TS 코드 없는(no-code) 빌더입니다. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 채팅/에이전트 UI | react-flow를 활용하여 실험 및 프로토타이핑을 손쉽게 할 수 있도록 디자인된 LangChain용 Python 기반 UI입니다. |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | 채팅/에이전트 UI | 코드 없는 빌더와 함께 제공되는 오픈 소스 LLM 애플리케이션 개발 플랫폼입니다. |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 채팅/에이전트 UI | 셀프 호스팅 및 로컬 모델 등 다양한 LLM 실행기를 지원하는 셀프 호스팅 LLM 채팅 웹 UI입니다. |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 도구 | 오픈 소스 LLM 테스트 플랫폼입니다. |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 채팅/에이전트 UI | 오픈 소스 챗봇 플랫폼입니다. |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 플랫폼 | 오픈 소스 음성 AI 플랫폼입니다. |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 채팅/에이전트 UI | 채팅 UI와 같은 웹 인터페이스 구축을 위한 오픈 소스 Python 라이브러리입니다. |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 에이전트 | 오픈 소스 AI 에이전트 프레임워크입니다. |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 에이전트 | 에이전트 간 협업 및 도구 사용을 위한 다중 에이전트 프레임워크입니다. |
|
||||
|
||||
## 🚀 빠른 시작
|
||||
|
||||
앱에 계측을 추가하고 Langfuse에 trace 데이터를 수집하여, LLM 호출 및 검색, 임베딩, 에이전트 동작과 같은 애플리케이션 로직을 추적해보세요. 복잡한 로그와 사용자 세션을 확인하여 디버깅할 수 있습니다.
|
||||
|
||||
### 1️⃣ 새 프로젝트 생성
|
||||
|
||||
1. [Langfuse 계정 생성](https://cloud.langfuse.com/auth/sign-up) 또는 [셀프 호스트](https://langfuse.com/self-hosting)
|
||||
2. 새 프로젝트를 생성합니다.
|
||||
3. 프로젝트 설정에서 새로운 API 자격 증명을 생성합니다.
|
||||
|
||||
### 2️⃣ 첫 번째 LLM 호출 기록하기
|
||||
|
||||
[`@observe()` 데코레이터](https://langfuse.com/docs/sdk/python/decorators)를 사용하면 Python LLM 애플리케이션의 추적이 매우 간편해집니다. 이 빠른 시작 예제에서는 Langfuse [OpenAI 통합](https://langfuse.com/integrations/model-providers/openai-py)을 사용하여 모든 모델 파라미터를 자동으로 캡처합니다.
|
||||
|
||||
> [!TIP]
|
||||
> OpenAI를 사용하지 않으시다면, 다른 모델 및 프레임워크의 로그 기록 방법은 [문서](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse)를 참조하세요.
|
||||
|
||||
```bash
|
||||
pip install langfuse openai
|
||||
```
|
||||
|
||||
```bash filename=".env"
|
||||
LANGFUSE_SECRET_KEY="sk-lf-..."
|
||||
LANGFUSE_PUBLIC_KEY="pk-lf-..."
|
||||
LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 EU region
|
||||
# LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" # 🇺🇸 US region
|
||||
```
|
||||
|
||||
```python:main.py
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI integration
|
||||
|
||||
@observe()
|
||||
def story():
|
||||
return openai.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=[{"role": "user", "content": "What is Langfuse?"}],
|
||||
).choices[0].message.content
|
||||
|
||||
@observe()
|
||||
def main():
|
||||
return story()
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
### 3️⃣ Langfuse에서 trace 확인하기
|
||||
|
||||
Langfuse에서 LLM 호출 및 애플리케이션의 기타 로직에 대한 trace를 확인할 수 있습니다.
|
||||
|
||||

|
||||
|
||||
_[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/2cec01e3-3dc2-472f-afcf-3b968cf0c1f4?timestamp=2025-02-10T14%3A27%3A30.275Z&observation=cb5ff844-07ef-41e6-b8e2-6c64344bc13b)_
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Langfuse의 trace에 대해 더 알아보거나 [인터랙티브 데모](https://langfuse.com/docs/demo)에서 직접 체험해보세요.
|
||||
|
||||
## ⭐️ 별을 눌러주세요
|
||||
|
||||

|
||||
|
||||
## 💭 지원
|
||||
|
||||
질문에 대한 답변을 찾는 방법:
|
||||
|
||||
- 우리의 [문서](https://langfuse.com/docs)는 답을 찾기 위한 최적의 장소입니다. 문서가 매우 포괄적이며, 유지보수에 많은 노력을 기울이고 있습니다. GitHub를 통해 문서 수정 제안도 가능합니다.
|
||||
- [Langfuse FAQ](https://langfuse.com/faq)에서는 가장 흔한 질문에 대해 답변하고 있습니다.
|
||||
- 질문에 즉각적인 답변이 필요하다면 [Ask AI](https://langfuse.com/docs/ask-ai)를 사용해보세요.
|
||||
|
||||
지원 채널:
|
||||
|
||||
- **GitHub Discussions의 [공개 Q&A](https://github.com/orgs/langfuse/discussions/categories/support)** 에 질문을 남겨주세요. 가능한 한 많은 세부 사항(예: 코드 스니펫, 스크린샷, 배경 정보)을 포함해 질문해 주시기 바랍니다.
|
||||
- [기능 요청](https://github.com/orgs/langfuse/discussions/categories/ideas)을 남겨주세요.
|
||||
- [버그 신고](https://github.com/langfuse/langfuse/issues)는 GitHub Issues를 통해 해주세요.
|
||||
- 긴급한 문의는 앱 내 채팅 위젯을 통해 연락 바랍니다.
|
||||
|
||||
## 🤝 기여하기
|
||||
|
||||
여러분의 기여를 환영합니다!
|
||||
|
||||
- GitHub Discussions의 [아이디어](https://github.com/orgs/langfuse/discussions/categories/ideas)에 투표해보세요.
|
||||
- GitHub Issues에서 이슈를 제기하고 댓글을 남겨주세요.
|
||||
- PR을 제출하세요 – 개발 환경 설정 방법 등 자세한 내용은 [CONTRIBUTING.md](CONTRIBUTING.md)를 참조하세요.
|
||||
|
||||
## 🥇 라이선스
|
||||
|
||||
이 저장소는 `ee` 폴더를 제외하고 MIT 라이선스가 적용됩니다. 자세한 내용은 [LICENSE](LICENSE)와 [문서](https://langfuse.com/docs/open-source)를 확인하세요.
|
||||
|
||||
## ⭐️ 별(Star) 히스토리
|
||||
|
||||
<a href="https://star-history.com/#langfuse/langfuse&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" style="border-radius: 15px;" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## ❤️ Langfuse를 사용하는 오픈 소스 프로젝트
|
||||
|
||||
별(star) 수를 기준으로 순위가 매겨진 Langfuse를 사용하는 상위 오픈 소스 Python 프로젝트들 ([출처](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
|
||||
|
||||
| 저장소 | 별 |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 보안 & 개인정보 보호
|
||||
|
||||
우리는 데이터 보안과 개인정보 보호를 매우 중요하게 생각합니다. 자세한 내용은 [Security and Privacy](https://langfuse.com/security) 페이지를 참조하세요.
|
||||
|
||||
### 텔레메트리
|
||||
|
||||
기본적으로 Langfuse는 자체 호스팅 인스턴스의 기본 사용 통계를 중앙 서버(PostHog)로 자동 보고합니다.
|
||||
|
||||
이를 통해:
|
||||
|
||||
1. Langfuse가 어떻게 사용되는지 이해하고, 가장 관련성 높은 기능을 개선할 수 있습니다.
|
||||
2. 내부 및 외부(예: 자금 조달) 보고를 위한 전체 사용량을 추적할 수 있습니다.
|
||||
|
||||
수집된 데이터는 제3자와 공유되지 않으며 민감한 정보를 포함하지 않습니다. 이에 대해 매우 투명하게 공개하고 있으며, 수집되는 정확한 데이터는 [여기](/web/src/features/telemetry/index.ts)에서 확인할 수 있습니다.
|
||||
|
||||
환경 변수 `TELEMETRY_ENABLED=false`를 설정하여 옵트아웃할 수 있습니다.
|
||||
@@ -1,381 +1,171 @@
|
||||
<img width="4856" height="1000" alt="github-banner" src="https://github.com/user-attachments/assets/6f435ef3-1194-4e26-87af-aa13826bbb5f" />
|
||||

|
||||
|
||||
<div align="center"><h1>Langfuse: Open Source LLM Engineering Platform</h1></div>
|
||||
<div align="center"><h4>LLM Observability, Prompt Management, LLM Evaluations,<br/>Datasets, LLM Metrics, and Prompt Playground</h4></div>
|
||||
|
||||
<div align="center">
|
||||
<div>
|
||||
<h3>
|
||||
<a href="https://langfuse.com/blog/2025-06-04-open-sourcing-langfuse-product">
|
||||
<strong>Langfuse Is Doubling Down On Open Source</strong>
|
||||
</a> <br> <br>
|
||||
<a href="https://cloud.langfuse.com">
|
||||
<strong>Langfuse Cloud</strong>
|
||||
<strong>Sign up</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/docs/deployment/self-host">
|
||||
<strong>Self Host</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/demo">
|
||||
<strong>Demo</strong>
|
||||
<strong>Demo (live data)</strong>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="https://langfuse.com/docs"><strong>Docs</strong></a> ·
|
||||
<a href="https://langfuse.com/issues"><strong>Report Bug</strong></a> ·
|
||||
<a href="https://langfuse.com/ideas"><strong>Feature Request</strong></a> ·
|
||||
<a href="https://langfuse.com/changelog"><strong>Changelog</strong></a> ·
|
||||
<a href="https://langfuse.com/roadmap"><strong>Roadmap</strong></a> ·
|
||||
<a href="https://langfuse.com/discord"><strong>Discord</strong></a>
|
||||
</div>
|
||||
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> for Support and Feature Requests.</span>
|
||||
<br/>
|
||||
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>GitHub Discussions</strong></a> for Support and Feature Requests.</span>
|
||||
<br/>
|
||||
<span><b>We're hiring.</b> <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in product engineering and technical go-to-market roles.</span>
|
||||
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in Product Engineering and Developer Relations.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License"></a>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?style=flat-square&logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?style=flat-square&logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
|
||||
</div>
|
||||
</div>
|
||||
</br>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-E11311.svg" alt="MIT License">
|
||||
</a>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange" alt="Y Combinator W23"></a>
|
||||
<a href="https://hub.docker.com/u/langfuse" target="_blank">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langfuse/langfuse?labelColor=%20%23FDB062&logo=Docker&labelColor=%20%23528bff"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
|
||||
<br/>
|
||||
<a href="https://discord.com/invite/7NXusRtqYU" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1111061815649124414?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="chat on Discord"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=langfuse" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/langfuse?logo=X&color=%20%23f5f5f5"
|
||||
alt="follow on X(Twitter)"></a>
|
||||
<a href="https://www.linkedin.com/company/langfuse/" target="_blank">
|
||||
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||
alt="follow on LinkedIn"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/graphs/commit-activity" target="_blank">
|
||||
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langfuse/langfuse?labelColor=%20%2332b583&color=%20%2312b76a"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/" target="_blank">
|
||||
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alangfuse%2Flangfuse%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/discussions/" target="_blank">
|
||||
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langfuse/langfuse?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
|
||||
<a href="https://deepwiki.com/langfuse/langfuse" target="_blank">
|
||||
<img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg"></a>
|
||||
</p>
|
||||
## Langfuse Overview
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||
<a href="./README.cn.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
||||
<a href="./README.ja.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
||||
<a href="./README.kr.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||
</p>
|
||||
_Unmute video for voice-over_
|
||||
|
||||
Langfuse is an **open source LLM engineering** platform. It helps teams collaboratively
|
||||
**develop, monitor, evaluate,** and **debug** AI applications. Langfuse can be **self-hosted in minutes** and is **battle-tested**.
|
||||
https://github.com/langfuse/langfuse/assets/2834609/a94062e9-c782-4ee9-af59-dee6370149a8
|
||||
|
||||
[](https://langfuse.com/watch-demo)
|
||||
### Develop
|
||||
|
||||
## ✨ Core Features
|
||||
- **LLM Observability:** Instrument your app and start ingesting traces to Langfuse ([Quickstart](https://langfuse.com/docs/get-started), [Integrations](https://langfuse.com/docs/integrations) [Tracing](https://langfuse.com/docs/tracing))
|
||||
- **Langfuse UI:** Inspect and debug complex logs ([Demo](https://langfuse.com/docs/demo), [Tracing](https://langfuse.com/docs/tracing))
|
||||
- **Prompt Management:** Manage, version and deploy prompts from within Langfuse ([Prompt Management](https://langfuse.com/docs/prompts/get-started))
|
||||
- **Prompt Engineering:** Test and iterate on your prompts with the [LLM Playground](https://langfuse.com/docs/playground)
|
||||
|
||||
<img width="4856" height="1944" alt="Langfuse Overview" src="https://github.com/user-attachments/assets/5dac68ef-d546-49fb-b06f-cfafc19282e3" />
|
||||
### Monitor
|
||||
|
||||
- [LLM Application Observability](https://langfuse.com/docs/tracing): Instrument your app and start ingesting traces to Langfuse, thereby tracking LLM calls and other relevant logic in your app such as retrieval, embedding, or agent actions. Inspect and debug complex logs and user sessions. Try the interactive [demo](https://langfuse.com/docs/demo) to see this in action.
|
||||
- **LLM Analytics:** Track metrics (cost, latency, quality) and gain insights from dashboards & data exports ([Analytics](https://langfuse.com/docs/analytics))
|
||||
- **LLM Evaluations:** Collect and calculate scores for your LLM completions ([Scores & Evaluations](https://langfuse.com/docs/scores))
|
||||
- Run ([Model-based evaluations](https://langfuse.com/docs/scores/model-based-evals)) and LLM-as-a-Judge within Langfuse
|
||||
- Collect user feedback ([User Feedback](https://langfuse.com/docs/scores/user-feedback))
|
||||
- Manually score LLM outputs in Langfuse ([Manual Scores](https://langfuse.com/docs/scores/manually))
|
||||
|
||||
- [Prompt Management](https://langfuse.com/docs/prompt-management/get-started) helps you centrally manage, version control, and collaboratively iterate on your prompts. Thanks to strong caching on server and client side, you can iterate on prompts without adding latency to your application.
|
||||
### Test
|
||||
|
||||
- [Evaluations](https://langfuse.com/docs/evaluation/overview) are key to the LLM application development workflow, and Langfuse adapts to your needs. It supports LLM-as-a-judge, user feedback collection, manual labeling, and custom evaluation pipelines via APIs/SDKs.
|
||||
- **Experiments:** Track and test app behaviour before deploying a new version
|
||||
- Datasets let you test expected in and output pairs and benchmark performance before deploying ([Datasets](https://langfuse.com/docs/datasets))
|
||||
- Track versions and releases in your application ([Experimentation](https://langfuse.com/docs/experimentation), [Prompt Management](https://langfuse.com/docs/prompts))
|
||||
|
||||
- [Datasets](https://langfuse.com/docs/evaluation/dataset-runs/datasets) enable test sets and benchmarks for evaluating your LLM application. They support continuous improvement, pre-deployment testing, structured experiments, flexible evaluation, and seamless integration with frameworks like LangChain and LlamaIndex.
|
||||
|
||||
- [LLM Playground](https://langfuse.com/docs/playground) is a tool for testing and iterating on your prompts and model configurations, shortening the feedback loop and accelerating development. When you see a bad result in tracing, you can directly jump to the playground to iterate on it.
|
||||
|
||||
- [Comprehensive API](https://langfuse.com/docs/api): Langfuse is frequently used to power bespoke LLMOps workflows while using the building blocks provided by Langfuse via the API. OpenAPI spec, Postman collection, and typed SDKs for Python, JS/TS are available.
|
||||
|
||||
## 📦 Deploy Langfuse
|
||||
|
||||
<img width="4856" height="1322" alt="Langfuse Deployment Options" src="https://github.com/user-attachments/assets/98f020c7-7a20-4264-a201-65c41a52a5d5" />
|
||||
## Get started
|
||||
|
||||
### Langfuse Cloud
|
||||
|
||||
Managed deployment by the Langfuse team, generous free-tier, no credit card required.
|
||||
Managed deployment by the Langfuse team, generous free-tier (hobby plan), no credit card required.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://cloud.langfuse.com" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/»%20Sign%20up%20for%20Langfuse%20Cloud-8A2BE2?&color=orange">
|
||||
</a>
|
||||
</div>
|
||||
**[» Langfuse Cloud](https://cloud.langfuse.com)**
|
||||
|
||||
### Self-Host Langfuse
|
||||
## Self-Hosting Open Source LLM Observability with Langfuse
|
||||
|
||||
Run Langfuse on your own infrastructure:
|
||||
### Localhost (docker)
|
||||
|
||||
- [Local (docker compose)](https://langfuse.com/self-hosting/local): Run Langfuse on your own machine in 5 minutes using Docker Compose.
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/langfuse/langfuse.git
|
||||
cd langfuse
|
||||
|
||||
```bash
|
||||
# Get a copy of the latest Langfuse repository
|
||||
git clone https://github.com/langfuse/langfuse.git
|
||||
cd langfuse
|
||||
# Run server and database
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
# Run the langfuse docker compose
|
||||
docker compose up
|
||||
```
|
||||
[→ Learn more about deploying locally](https://langfuse.com/docs/deployment/local)
|
||||
|
||||
- [VM](https://langfuse.com/self-hosting/docker-compose): Run Langfuse on a single Virtual Machine using Docker Compose.
|
||||
- [Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm): Run Langfuse on a Kubernetes cluster using Helm. This is the preferred production deployment.
|
||||
- Terraform Templates: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
|
||||
### Self-host (docker)
|
||||
|
||||
See [self-hosting documentation](https://langfuse.com/self-hosting) to learn more about architecture and configuration options.
|
||||
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container and a postgres database.
|
||||
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
|
||||
|
||||
## 🔌 Integrations
|
||||
Templated deployments: [Railway, GCP, AWS, Azure, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
|
||||
|
||||
<img width="4856" height="1322" alt="github-integrations" src="https://github.com/user-attachments/assets/e41ea0fb-742d-41ce-bf94-1d4fb95750cd" />
|
||||
## Get Started
|
||||
|
||||
### Main Integrations:
|
||||
### API Keys
|
||||
|
||||
You need a Langfuse public and secret key to get started. Sign up [here](https://cloud.langfuse.com) and find them in your project settings.
|
||||
|
||||
### Ingesting Data · Instrumenting Your Application · LLM Observability with Langfuse
|
||||
|
||||
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python (Decorators)](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
|
||||
|
||||
See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Langfuse.
|
||||
|
||||
### LLM Observability Integrations
|
||||
|
||||
| Integration | Supports | Description |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
|
||||
| [Mastra](https://langfuse.com/docs/integrations/mastra) | JS/TS | Open source framework for building AI agents and multi-agent systems. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
### Packages integrated with Langfuse:
|
||||
Packages integrated with Langfuse:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | Library | Library to get structured LLM outputs (JSON, Pydantic) |
|
||||
| [DSPy](https://langfuse.com/docs/integrations/dspy) | Library | Framework that systematically optimizes language model prompts and weights |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | Library | Python toolkit for building LLM applications. |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | Model (local) | Easily run open source LLMs on your own machine. |
|
||||
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | Model | Run foundation and fine-tuned models on AWS. |
|
||||
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | Agent Framework | Open source LLM platform for building distributed agents. |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | Chat/Agent UI | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Chat/Agent UI | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | Chat/Agent UI | Open source LLM app development platform with no-code builder. |
|
||||
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | Chat/Agent UI | Self-hosted LLM Chat web ui supporting various LLM runners including self-hosted and local models. |
|
||||
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | Tool | Open source LLM testing platform. |
|
||||
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | Chat/Agent UI | Open source chatbot platform. |
|
||||
| [Vapi](https://langfuse.com/docs/integrations/vapi) | Platform | Open source voice AI platform. |
|
||||
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | Agents | Open source LLM platform for building distributed agents. |
|
||||
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | Chat/Agent UI | Open source Python library to build web interfaces like Chat UI. |
|
||||
| [Goose](https://langfuse.com/docs/integrations/goose) | Agents | Open source LLM platform for building distributed agents. |
|
||||
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | Agents | Open source AI agents framework. |
|
||||
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | Agents | Multi agent framework for agent collaboration and tool use. |
|
||||
| Name | Description |
|
||||
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | Open source LLM app development platform with no-code builder. |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | Easily run open source LLMs on your own machine. |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
|
||||
## 🚀 Quickstart
|
||||
## Questions and feedback
|
||||
|
||||
Instrument your app and start ingesting traces to Langfuse, thereby tracking LLM calls and other relevant logic in your app such as retrieval, embedding, or agent actions. Inspect and debug complex logs and user sessions.
|
||||
### Ideas and roadmap
|
||||
|
||||
### 1️⃣ Create new project
|
||||
- [Roadmap](https://langfuse.com/roadmap)
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
|
||||
- [Feature Requests](https://langfuse.com/ideas)
|
||||
|
||||
1. [Create Langfuse account](https://cloud.langfuse.com/auth/sign-up) or [self-host](https://langfuse.com/self-hosting)
|
||||
2. Create a new project
|
||||
3. Create new API credentials in the project settings
|
||||
### Support and feedback
|
||||
|
||||
### 2️⃣ Log your first LLM call
|
||||
In order of preference the best way to communicate with us:
|
||||
|
||||
The [`@observe()` decorator](https://langfuse.com/docs/sdk/python/decorators) makes it easy to trace any Python LLM application. In this quickstart we also use the Langfuse [OpenAI integration](https://langfuse.com/integrations/model-providers/openai-py) to automatically capture all model parameters.
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions) (preferred): Contribute [ideas](https://langfuse.com/ideas), [support requests](https://langfuse.com/gh-support) and [report bugs](https://langfuse.com/issues)
|
||||
- [Discord](https://langfuse.com/discord): community support
|
||||
- Privately: contact at langfuse dot com
|
||||
|
||||
> [!TIP]
|
||||
> Not using OpenAI? Visit [our documentation](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse) to learn how to log other models and frameworks.
|
||||
## Contributing to Langfuse
|
||||
|
||||
```bash
|
||||
pip install langfuse openai
|
||||
```
|
||||
|
||||
```bash filename=".env"
|
||||
LANGFUSE_SECRET_KEY="sk-lf-..."
|
||||
LANGFUSE_PUBLIC_KEY="pk-lf-..."
|
||||
LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 EU region
|
||||
# LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" # 🇺🇸 US region
|
||||
```
|
||||
|
||||
```python /@observe()/ /from langfuse.openai import openai/ filename="main.py"
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI integration
|
||||
|
||||
@observe()
|
||||
def story():
|
||||
return openai.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=[{"role": "user", "content": "What is Langfuse?"}],
|
||||
).choices[0].message.content
|
||||
|
||||
@observe()
|
||||
def main():
|
||||
return story()
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
### 3️⃣ See traces in Langfuse
|
||||
|
||||
See your language model calls and other application logic in Langfuse.
|
||||
|
||||
<img width="1787" height="674" alt="Example trace in Langfuse" src="https://github.com/user-attachments/assets/f796eb78-dfb5-4570-b236-bdb4b67d4d55" />
|
||||
|
||||
_[Public example trace in Langfuse](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/2cec01e3-3dc2-472f-afcf-3b968cf0c1f4?timestamp=2025-02-10T14%3A27%3A30.275Z&observation=cb5ff844-07ef-41e6-b8e2-6c64344bc13b)_
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [Learn more](https://langfuse.com/docs/tracing) about tracing in Langfuse or play with the [interactive demo](https://langfuse.com/docs/demo).
|
||||
|
||||
## ⭐️ Star Us
|
||||
|
||||

|
||||
|
||||
## 💭 Support
|
||||
|
||||
Finding an answer to your question:
|
||||
|
||||
- Our [documentation](https://langfuse.com/docs) is the best place to start looking for answers. It is comprehensive, and we invest significant time into maintaining it. You can also suggest edits to the docs via GitHub.
|
||||
- [Langfuse FAQs](https://langfuse.com/faq) where the most common questions are answered.
|
||||
- Use "[Ask AI](https://langfuse.com/docs/ask-ai)" to get instant answers to your questions.
|
||||
|
||||
Support Channels:
|
||||
|
||||
- **Ask any question in our [public Q&A](https://github.com/orgs/langfuse/discussions/categories/support) on GitHub Discussions.** Please include as much detail as possible (e.g. code snippets, screenshots, background information) to help us understand your question.
|
||||
- [Request a feature](https://github.com/orgs/langfuse/discussions/categories/ideas) on GitHub Discussions.
|
||||
- [Report a Bug](https://github.com/langfuse/langfuse/issues) on GitHub Issues.
|
||||
- For time-sensitive queries, ping us via the in-app chat widget.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Your contributions are welcome!
|
||||
|
||||
- Vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas) in GitHub Discussions.
|
||||
- Raise and comment on [Issues](https://github.com/langfuse/langfuse/issues).
|
||||
- Vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
|
||||
- Raise and comment on [Issues](https://github.com/langfuse/langfuse/issues)
|
||||
- Open a PR - see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
|
||||
|
||||
## 🥇 License
|
||||
## License
|
||||
|
||||
This repository is MIT licensed, except for the `ee` folders. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
## Dependencies
|
||||
## Misc
|
||||
|
||||
We deploy this code base in Docker containers based on the Linux Alpine Image ([source](https://github.com/nodejs/docker-node)). You may find the Dockerfiles in [web/Dockerfile](web/Dockerfile) and [worker/Dockerfile](worker/Dockerfile).
|
||||
### GET API to export your data
|
||||
|
||||
## ⭐️ Star History
|
||||
[**GET routes**](https://langfuse.com/docs/integrations/api) to use data in downstream applications (e.g. embedded analytics). You can also access them conveniently via the SDKs ([docs](https://langfuse.com/docs/query-traces)).
|
||||
|
||||
<a href="https://star-history.com/#langfuse/langfuse&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" style="border-radius: 15px;" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## ❤️ Open Source Projects Using Langfuse
|
||||
|
||||
Top open-source Python projects that use Langfuse, ranked by stars ([Source](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
|
||||
|
||||
| Repository | Stars |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 116251 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 109642 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/23818?s=40&v=4" width="20" height="20" alt=""> [abi](https://github.com/abi) / [screenshot-to-code](https://github.com/abi/screenshot-to-code) | 70877 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 65454 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/69962740?s=40&v=4" width="20" height="20" alt=""> [infiniflow](https://github.com/infiniflow) / [ragflow](https://github.com/infiniflow/ragflow) | 64118 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/135057108?s=40&v=4" width="20" height="20" alt=""> [firecrawl](https://github.com/firecrawl) / [firecrawl](https://github.com/firecrawl/firecrawl) | 56713 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 44203 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 43547 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/159330290?s=40&v=4" width="20" height="20" alt=""> [QuivrHQ](https://github.com/QuivrHQ) / [quivr](https://github.com/QuivrHQ/quivr) | 38415 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/6154722?s=40&v=4" width="20" height="20" alt=""> [microsoft](https://github.com/microsoft) / [ai-agents-for-beginners](https://github.com/microsoft/ai-agents-for-beginners) | 38012 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 36071 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 35669 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 28726 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 22447 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/487568?s=40&v=4" width="20" height="20" alt=""> [NixOS](https://github.com/NixOS) / [nixpkgs](https://github.com/NixOS/nixpkgs) | 21748 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/170767358?s=40&v=4" width="20" height="20" alt=""> [kortix-ai](https://github.com/kortix-ai) / [suna](https://github.com/kortix-ai/suna) | 17976 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 17057 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/149120496?s=40&v=4" width="20" height="20" alt=""> [mastra-ai](https://github.com/mastra-ai) / [mastra](https://github.com/mastra-ai/mastra) | 16484 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/134601687?s=40&v=4" width="20" height="20" alt=""> [langfuse](https://github.com/langfuse) / [langfuse](https://github.com/langfuse/langfuse) | 16054 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 11868 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 8350 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/193350244?s=40&v=4" width="20" height="20" alt=""> [The-Pocket](https://github.com/The-Pocket) / [PocketFlow](https://github.com/The-Pocket/PocketFlow) | 8313 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139012218?s=40&v=4" width="20" height="20" alt=""> [OpenPipe](https://github.com/OpenPipe) / [ART](https://github.com/OpenPipe/ART) | 7093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 7011 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/3299148?s=40&v=4" width="20" height="20" alt=""> [awslabs](https://github.com/awslabs) / [agent-squad](https://github.com/awslabs/agent-squad) | 6785 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/162546372?s=40&v=4" width="20" height="20" alt=""> [BasedHardware](https://github.com/BasedHardware) / [omi](https://github.com/BasedHardware/omi) | 6231 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/118301880?s=40&v=4" width="20" height="20" alt=""> [hatchet-dev](https://github.com/hatchet-dev) / [hatchet](https://github.com/hatchet-dev/hatchet) | 6019 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/88676955?s=40&v=4" width="20" height="20" alt=""> [zenml-io](https://github.com/zenml-io) / [zenml](https://github.com/zenml-io/zenml) | 4873 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132635806?s=40&v=4" width="20" height="20" alt=""> [refly-ai](https://github.com/refly-ai) / [refly](https://github.com/refly-ai/refly) | 4654 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/47287758?s=40&v=4" width="20" height="20" alt=""> [coleam00](https://github.com/coleam00) / [ottomator-agents](https://github.com/coleam00/ottomator-agents) | 4165 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/8251002?s=40&v=4" width="20" height="20" alt=""> [JoshuaC215](https://github.com/JoshuaC215) / [agent-service-toolkit](https://github.com/JoshuaC215/agent-service-toolkit) | 3557 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185852128?s=40&v=4" width="20" height="20" alt=""> [colanode](https://github.com/colanode) / [colanode](https://github.com/colanode/colanode) | 3517 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/201282378?s=40&v=4" width="20" height="20" alt=""> [VoltAgent](https://github.com/VoltAgent) / [voltagent](https://github.com/VoltAgent/voltagent) | 3210 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bragai](https://github.com/bragai) / [bRAG-langchain](https://github.com/bragai/bRAG-langchain) | 3010 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2651 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/135396723?s=40&v=4" width="20" height="20" alt=""> [sourcebot-dev](https://github.com/sourcebot-dev) / [sourcebot](https://github.com/sourcebot-dev/sourcebot) | 2570 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 2055 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179994263?s=40&v=4" width="20" height="20" alt=""> [YFGaia](https://github.com/YFGaia) / [dify-plus](https://github.com/YFGaia/dify-plus) | 1734 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/46323662?s=40&v=4" width="20" height="20" alt=""> [TheSpaghettiDetective](https://github.com/TheSpaghettiDetective) / [obico-server](https://github.com/TheSpaghettiDetective/obico-server) | 1687 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1387 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/144196744?s=40&v=4" width="20" height="20" alt=""> [TIGER-AI-Lab](https://github.com/TIGER-AI-Lab) / [TheoremExplainAgent](https://github.com/TIGER-AI-Lab/TheoremExplainAgent) | 1385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2314423?s=40&v=4" width="20" height="20" alt=""> [trailofbits](https://github.com/trailofbits) / [buttercup](https://github.com/trailofbits/buttercup) | 1223 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/62564480?s=40&v=4" width="20" height="20" alt=""> [wassim249](https://github.com/wassim249) / [fastapi-langgraph-agent-production-ready-template](https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template) | 1200 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1098 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 1010 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/65890619?s=40&v=4" width="20" height="20" alt=""> [zstar1003](https://github.com/zstar1003) / [ragflow-plus](https://github.com/zstar1003/ragflow-plus) | 993 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121352441?s=40&v=4" width="20" height="20" alt=""> [openops-cloud](https://github.com/openops-cloud) / [openops](https://github.com/openops-cloud/openops) | 939 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 927 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/74420131?s=40&v=4" width="20" height="20" alt=""> [xataio](https://github.com/xataio) / [agent](https://github.com/xataio/agent) | 857 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/123981229?s=40&v=4" width="20" height="20" alt=""> [plastic-labs](https://github.com/plastic-labs) / [tutor-gpt](https://github.com/plastic-labs/tutor-gpt) | 845 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/176438967?s=40&v=4" width="20" height="20" alt=""> [trendy-design](https://github.com/trendy-design) / [llmchat](https://github.com/trendy-design/llmchat) | 829 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2045570?s=40&v=4" width="20" height="20" alt=""> [hotovo](https://github.com/hotovo) / [aider-desk](https://github.com/hotovo/aider-desk) | 781 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 719 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/142796018?s=40&v=4" width="20" height="20" alt=""> [wrtnlabs](https://github.com/wrtnlabs) / [autoview](https://github.com/wrtnlabs/autoview) | 688 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 643 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 587 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 487 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/58037050?s=40&v=4" width="20" height="20" alt=""> [NicholasGoh](https://github.com/NicholasGoh) / [fastapi-mcp-langgraph-template](https://github.com/NicholasGoh/fastapi-mcp-langgraph-template) | 481 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 472 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/163431636?s=40&v=4" width="20" height="20" alt=""> [AIDotNet](https://github.com/AIDotNet) / [koala-ai](https://github.com/AIDotNet/koala-ai) | 470 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [text-analytics-legacy](https://github.com/phospho-app/text-analytics-legacy) | 439 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/170831637?s=40&v=4" width="20" height="20" alt=""> [inferablehq](https://github.com/inferablehq) / [inferable](https://github.com/inferablehq/inferable) | 403 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171800766?s=40&v=4" width="20" height="20" alt=""> [duoyang666](https://github.com/duoyang666) / [ai_novel](https://github.com/duoyang666/ai_novel) | 397 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/209155962?s=40&v=4" width="20" height="20" alt=""> [strands-agents](https://github.com/strands-agents) / [samples](https://github.com/strands-agents/samples) | 385 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/16997807?s=40&v=4" width="20" height="20" alt=""> [FranciscoMoretti](https://github.com/FranciscoMoretti) / [sparka](https://github.com/FranciscoMoretti/sparka) | 380 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 373 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137044109?s=40&v=4" width="20" height="20" alt=""> [ElectricCodeGuy](https://github.com/ElectricCodeGuy) / [SupabaseAuthWithSSR](https://github.com/ElectricCodeGuy/SupabaseAuthWithSSR) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 323 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/8931462?s=40&v=4" width="20" height="20" alt=""> [aws-samples](https://github.com/aws-samples) / [aws-ai-ml-workshop-kr](https://github.com/aws-samples/aws-ai-ml-workshop-kr) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/25676773?s=40&v=4" width="20" height="20" alt=""> [weizxfree](https://github.com/weizxfree) / [KnowFlow](https://github.com/weizxfree/KnowFlow) | 285 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/88676955?s=40&v=4" width="20" height="20" alt=""> [zenml-io](https://github.com/zenml-io) / [zenml-projects](https://github.com/zenml-io/zenml-projects) | 276 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/136071305?s=40&v=4" width="20" height="20" alt=""> [wxai-space](https://github.com/wxai-space) / [LightAgent](https://github.com/wxai-space/LightAgent) | 275 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/90278288?s=40&v=4" width="20" height="20" alt=""> [Ozamatash](https://github.com/Ozamatash) / [deep-research-mcp](https://github.com/Ozamatash/deep-research-mcp) | 269 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 241 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/828269?s=40&v=4" width="20" height="20" alt=""> [guyernest](https://github.com/guyernest) / [advanced-rag](https://github.com/guyernest/advanced-rag) | 238 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/175069511?s=40&v=4" width="20" height="20" alt=""> [bklieger-groq](https://github.com/bklieger-groq) / [mathtutor-on-groq](https://github.com/bklieger-groq/mathtutor-on-groq) | 233 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/123981229?s=40&v=4" width="20" height="20" alt=""> [plastic-labs](https://github.com/plastic-labs) / [honcho](https://github.com/plastic-labs/honcho) | 224 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/118967077?s=40&v=4" width="20" height="20" alt=""> [OVINC-CN](https://github.com/OVINC-CN) / [OpenWebUI](https://github.com/OVINC-CN/OpenWebUI) | 202 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31960995?s=40&v=4" width="20" height="20" alt=""> [zhutoutoutousan](https://github.com/zhutoutoutousan) / [worldquant-miner](https://github.com/zhutoutoutousan/worldquant-miner) | 202 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/29215115?s=40&v=4" width="20" height="20" alt=""> [iceener](https://github.com/iceener) / [ai](https://github.com/iceener/ai) | 186 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/187584218?s=40&v=4" width="20" height="20" alt=""> [giselles-ai](https://github.com/giselles-ai) / [giselle](https://github.com/giselles-ai/giselle) | 181 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/174666116?s=40&v=4" width="20" height="20" alt=""> [ai-shifu](https://github.com/ai-shifu) / [ai-shifu](https://github.com/ai-shifu/ai-shifu) | 181 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/8931462?s=40&v=4" width="20" height="20" alt=""> [aws-samples](https://github.com/aws-samples) / [sample-serverless-mcp-servers](https://github.com/aws-samples/sample-serverless-mcp-servers) | 175 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/202488052?s=40&v=4" width="20" height="20" alt=""> [celerforge](https://github.com/celerforge) / [freenote](https://github.com/celerforge/freenote) | 171 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 164 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 163 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 163 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/1009716?s=40&v=4" width="20" height="20" alt=""> [codecentric](https://github.com/codecentric) / [c4-genai-suite](https://github.com/codecentric/c4-genai-suite) | 152 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/196509932?s=40&v=4" width="20" height="20" alt=""> [XSpoonAi](https://github.com/XSpoonAi) / [spoon-core](https://github.com/XSpoonAi/spoon-core) | 150 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [LangGraph-Chatchat](https://github.com/chatchat-space/LangGraph-Chatchat) | 144 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/134601687?s=40&v=4" width="20" height="20" alt=""> [langfuse](https://github.com/langfuse) / [langfuse-docs](https://github.com/langfuse/langfuse-docs) | 139 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/44976328?s=40&v=4" width="20" height="20" alt=""> [piyushgarg-dev](https://github.com/piyushgarg-dev) / [genai-cohort](https://github.com/piyushgarg-dev/genai-cohort) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105285801?s=40&v=4" width="20" height="20" alt=""> [i-dot-ai](https://github.com/i-dot-ai) / [redbox](https://github.com/i-dot-ai/redbox) | 132 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/90423581?s=40&v=4" width="20" height="20" alt=""> [bmd1905](https://github.com/bmd1905) / [ChatOpsLLM](https://github.com/bmd1905/ChatOpsLLM) | 127 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/202074444?s=40&v=4" width="20" height="20" alt=""> [Fintech-Dreamer](https://github.com/Fintech-Dreamer) / [FinSynth](https://github.com/Fintech-Dreamer/FinSynth) | 121 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/1566555?s=40&v=4" width="20" height="20" alt=""> [kenshiro-o](https://github.com/kenshiro-o) / [nagato-ai](https://github.com/kenshiro-o/nagato-ai) | 119 |
|
||||
|
||||
## 🔒 Security & Privacy
|
||||
### Security & Privacy
|
||||
|
||||
We take data security and privacy seriously. Please refer to our [Security and Privacy](https://langfuse.com/security) page for more information.
|
||||
|
||||
@@ -391,3 +181,13 @@ This helps us to:
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/web/src/features/telemetry/index.ts).
|
||||
|
||||
You can opt-out by setting `TELEMETRY_ENABLED=false`.
|
||||
|
||||
### Star History
|
||||
|
||||
<a href="https://star-history.com/#langfuse/langfuse&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# Code Review Instructions
|
||||
|
||||
## Database Migrations
|
||||
|
||||
### ClickHouse
|
||||
|
||||
- ClickHouse migrations in the `packages/shared/clickhouse/migrations/clustered` directory should include `ON CLUSTER default` and should use `Replicated` merge tree table types.
|
||||
- E.g. `ReplacingMergeTree` is likely an error while `ReplicatedReplacingMergeTree` would be correct in most cases.
|
||||
- ClickHouse migrations in the `packages/shared/clickhouse/migrations/unclustered` directory must not include `ON CLUSTER` statements and must not use `Replicated` merge tree table types.
|
||||
- Migrations in `packages/shared/clickhouse/migrations/clustered` should match their counterparts in `packages/shared/clickhouse/migrations/unclustered` aside from the restrictions listed above.
|
||||
- When adding new indexes on ClickHouse, ensure that there is a corresponding `MATERIALIZE INDEX` statement in the same migration. The materialization can use `SETTINGS mutations_sync = 2` if they operate on smaller tables, but may timeout otherwise.
|
||||
- All ClickHouse queries on project-scoped tables (traces, observations, scores, events, sessions, etc.) must include `WHERE project_id = {projectId: String}` filter to ensure proper tenant isolation and that queries only access data from the intended project.
|
||||
- For operations on the `events` table, you must never use the `FINAL` keyword as it kills performance. `events` is built so that `FINAL` is never required.
|
||||
|
||||
### Postgres
|
||||
|
||||
- Most `schema.prisma` changes should produce a change in `packages/shared/prisma/migrations`.
|
||||
- All Prisma queries on project-scoped tables must include `projectId` in the WHERE clause (e.g., `where: { id: traceId, projectId }`) to ensure proper tenant isolation and that queries only access data from the intended project.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- Environment variables should be imported from the `env.mjs/ts` file of the respective package and not from `process.env.*` to ensure validation and typing.
|
||||
|
||||
## Redis Invocations
|
||||
|
||||
- Highlight usage of `redis.call` invocations. Those may have suboptimal redis cluster routing and will raise errors. Instead, use the native call patterns.
|
||||
Example: `await redis?.call("SET", key, "1", "NX", "EX", TTLSeconds);` should use `await redis?.set(key, "1", "EX", TTLSeconds, "NX");` instead.
|
||||
|
||||
## Langfuse Cloud
|
||||
|
||||
- When attempting to confirm if the current environment is Langfuse Cloud in the frontend, use the `useLangfuseCloudRegion` hook and never environment variables directly.
|
||||
|
||||
## Banner Height System
|
||||
|
||||
- Use `top-banner-offset` instead of `top-0` for any elements that are positioned `sticky`, `fixed`, or `absolute` with a global reference point (e.g., `top-0`). This ensures proper spacing when system banners (payment, maintenance, etc.) are displayed.
|
||||
- The banner height is managed through CSS variables (`--banner-height` and `--banner-offset`) defined in `web/src/styles/globals.css`.
|
||||
- Banner components (like PaymentBanner) dynamically update `--banner-height` using ResizeObserver to track their actual height, ensuring accurate positioning even when banners resize (e.g., on mobile wrapping).
|
||||
- Available Tailwind utilities:
|
||||
- `top-banner-offset` / `pt-banner-offset` - For sticky/fixed/absolute positioning and padding
|
||||
- `h-screen-with-banner` / `min-h-screen-with-banner` - For full-height containers accounting for banners
|
||||
|
||||
## JavaScript / TypeScript Style
|
||||
|
||||
- use concat instead of spread to avoid stack overflow with large arrays
|
||||
|
||||
## Seeder
|
||||
|
||||
- make sure that for new features with data model changes, the database seeder is adjusted.
|
||||
|
||||
## API Documentation
|
||||
|
||||
- Whenever a file in `web/src/features/public-api/types` changes, the `fern/apis` definition probably needs to be adjusted, too.
|
||||
- `nullish` types should map to `optional<nullable<T>>` in fern.
|
||||
- `nullable` types should map to `nullable<T>` in fern.
|
||||
- `optional` types should map to `optional<T>` in fern.
|
||||
@@ -0,0 +1,13 @@
|
||||
_We are Hiring_
|
||||
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
|
||||
|
||||
_Open Roles_
|
||||
|
||||
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
|
||||
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
|
||||
|
||||
_More Info_
|
||||
|
||||
- https://langfuse.com/careers
|
||||
- https://langfuse.com/docs
|
||||
- https://langfuse.com/changelog
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
+44
-117
@@ -1,57 +1,34 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
langfuse-web:
|
||||
server:
|
||||
build:
|
||||
dockerfile: ./web/Dockerfile
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
depends_on: &langfuse-depends-on
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment: &langfuse-web-env
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/postgres}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-mysecret}
|
||||
SALT: ${SALT:-mysalt}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000} # generate via `openssl rand -hex 32`
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
|
||||
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
|
||||
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
|
||||
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
|
||||
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
|
||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
|
||||
REDIS_TLS_ENABLED: ${REDIS_TLS_ENABLED:-false}
|
||||
REDIS_TLS_CA: ${REDIS_TLS_CA:-/certs/ca.crt}
|
||||
REDIS_TLS_CERT: ${REDIS_TLS_CERT:-/certs/redis.crt}
|
||||
REDIS_TLS_KEY: ${REDIS_TLS_KEY:-/certs/redis.key}
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
|
||||
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
|
||||
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
|
||||
@@ -59,17 +36,26 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
langfuse-worker:
|
||||
worker:
|
||||
build:
|
||||
dockerfile: ./worker/Dockerfile
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
depends_on: *langfuse-depends-on
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
ports:
|
||||
- "3030:3030"
|
||||
environment:
|
||||
<<: *langfuse-web-env
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- PORT=${PORT:-3030}
|
||||
- REDIS_HOST=${REDIS_HOST:-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
|
||||
@@ -77,85 +63,26 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
clickhouse:
|
||||
image: docker.io/clickhouse/clickhouse-server
|
||||
user: "101:101"
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- "8123:8123"
|
||||
- "9000:9000"
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 1s
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-miniosecret}
|
||||
ports:
|
||||
- "9090:9000"
|
||||
- "9091:9001"
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
|
||||
redis:
|
||||
image: docker.io/redis:7
|
||||
restart: always
|
||||
image: redis:7.2.4
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
--maxmemory-policy noeviction
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
postgres:
|
||||
image: docker.io/postgres:${POSTGRES_VERSION:-17}
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-postgres}
|
||||
TZ: UTC
|
||||
PGTZ: UTC
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
- database_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
langfuse_postgres_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
database_data:
|
||||
driver: local
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
services:
|
||||
clickhouse:
|
||||
image: docker.io/clickhouse/clickhouse-server:24.3
|
||||
image: clickhouse/clickhouse-server
|
||||
user: "101:101"
|
||||
container_name: clickhouse
|
||||
hostname: clickhouse
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: clickhouse
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
@@ -17,46 +19,23 @@ services:
|
||||
|
||||
azurite:
|
||||
image: mcr.microsoft.com/azure-storage/azurite
|
||||
container_name: azurite
|
||||
command: azurite-blob --blobHost 0.0.0.0
|
||||
ports:
|
||||
- "10000:10000"
|
||||
volumes:
|
||||
- langfuse_azurite_data:/data
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
container_name: ${MINIO_CONTAINER_NAME:-langfuse-minio}
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-miniosecret}
|
||||
ports:
|
||||
- ${HOST_IP:-127.0.0.1}:${MINIO_API_PORT:-9090}:9000
|
||||
- ${HOST_IP:-127.0.0.1}:${MINIO_CONSOLE_PORT:-9091}:9001
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
networks:
|
||||
- default
|
||||
|
||||
redis:
|
||||
image: docker.io/redis:7.2.4
|
||||
image: redis:7.2.4
|
||||
restart: always
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
--maxmemory-policy noeviction
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: docker.io/postgres:${POSTGRES_VERSION:-17}
|
||||
image: postgres:${POSTGRES_VERSION:-latest}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
@@ -65,11 +44,9 @@ services:
|
||||
retries: 10
|
||||
command: ["postgres", "-c", "log_statement=all"]
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-postgres}
|
||||
- TZ=UTC
|
||||
- PGTZ=UTC
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
@@ -84,6 +61,3 @@ volumes:
|
||||
driver: local
|
||||
langfuse_azurite_data:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
name: ${MINIO_VOLUME_NAME:-langfuse_minio_data}
|
||||
driver: local
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
services:
|
||||
clickhouse:
|
||||
image: docker.io/clickhouse/clickhouse-server:24.3
|
||||
user: "101:101"
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- 127.0.0.1:8123:8123
|
||||
- 127.0.0.1:9000:9000
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-miniosecret}
|
||||
ports:
|
||||
- 127.0.0.1:9090:9000
|
||||
- 127.0.0.1:9091:9001
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
|
||||
postgres:
|
||||
image: docker.io/postgres:${POSTGRES_VERSION:-17}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
command: ["postgres", "-c", "log_statement=all"]
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-postgres}
|
||||
- TZ=UTC
|
||||
- PGTZ=UTC
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
volumes:
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
# Redis Cluster. Requires a Unix host to work for network_mode: host. I.e. tests will fail on windows and mac with this setup.
|
||||
redis-node-0:
|
||||
image: docker.io/bitnamilegacy/redis-cluster:8.0
|
||||
network_mode: host
|
||||
volumes:
|
||||
- redis-cluster_data-0:/bitnami/redis/data
|
||||
environment:
|
||||
REDIS_PASSWORD: bitnami
|
||||
REDIS_PORT_NUMBER: 6370
|
||||
REDIS_NODES: 127.0.0.1:6370 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375
|
||||
|
||||
redis-node-1:
|
||||
image: docker.io/bitnamilegacy/redis-cluster:8.0
|
||||
network_mode: host
|
||||
volumes:
|
||||
- redis-cluster_data-1:/bitnami/redis/data
|
||||
environment:
|
||||
REDIS_PASSWORD: bitnami
|
||||
REDIS_PORT_NUMBER: 6371
|
||||
REDIS_NODES: 127.0.0.1:6370 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375
|
||||
|
||||
redis-node-2:
|
||||
image: docker.io/bitnamilegacy/redis-cluster:8.0
|
||||
network_mode: host
|
||||
volumes:
|
||||
- redis-cluster_data-2:/bitnami/redis/data
|
||||
environment:
|
||||
REDIS_PASSWORD: bitnami
|
||||
REDIS_PORT_NUMBER: 6372
|
||||
REDIS_NODES: 127.0.0.1:6370 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375
|
||||
|
||||
redis-node-3:
|
||||
image: docker.io/bitnamilegacy/redis-cluster:8.0
|
||||
network_mode: host
|
||||
volumes:
|
||||
- redis-cluster_data-3:/bitnami/redis/data
|
||||
environment:
|
||||
REDIS_PASSWORD: bitnami
|
||||
REDIS_PORT_NUMBER: 6373
|
||||
REDIS_NODES: 127.0.0.1:6370 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375
|
||||
|
||||
redis-node-4:
|
||||
image: docker.io/bitnamilegacy/redis-cluster:8.0
|
||||
network_mode: host
|
||||
volumes:
|
||||
- redis-cluster_data-4:/bitnami/redis/data
|
||||
environment:
|
||||
REDIS_PASSWORD: bitnami
|
||||
REDIS_PORT_NUMBER: 6374
|
||||
REDIS_NODES: 127.0.0.1:6370 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375
|
||||
|
||||
redis-node-5:
|
||||
image: docker.io/bitnamilegacy/redis-cluster:8.0
|
||||
network_mode: host
|
||||
volumes:
|
||||
- redis-cluster_data-5:/bitnami/redis/data
|
||||
depends_on:
|
||||
- redis-node-0
|
||||
- redis-node-1
|
||||
- redis-node-2
|
||||
- redis-node-3
|
||||
- redis-node-4
|
||||
environment:
|
||||
REDISCLI_AUTH: bitnami
|
||||
REDIS_CLUSTER_REPLICAS: 1
|
||||
REDIS_PASSWORD: bitnami
|
||||
REDIS_PORT_NUMBER: 6375
|
||||
REDIS_NODES: 127.0.0.1:6370 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375
|
||||
REDIS_CLUSTER_CREATOR: yes
|
||||
|
||||
volumes:
|
||||
langfuse_postgres_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
driver: local
|
||||
redis-cluster_data-0:
|
||||
driver: local
|
||||
redis-cluster_data-1:
|
||||
driver: local
|
||||
redis-cluster_data-2:
|
||||
driver: local
|
||||
redis-cluster_data-3:
|
||||
driver: local
|
||||
redis-cluster_data-4:
|
||||
driver: local
|
||||
redis-cluster_data-5:
|
||||
driver: local
|
||||
+20
-43
@@ -1,37 +1,34 @@
|
||||
services:
|
||||
clickhouse:
|
||||
# Upgrade from 24.3 to check behaviour on new events table.
|
||||
# Unit tests still verify 24.3 behaviour using -azure and -redis-cluster configs.
|
||||
image: docker.io/clickhouse/clickhouse-server:25.8
|
||||
image: clickhouse/clickhouse-server
|
||||
user: "101:101"
|
||||
container_name: ${CLICKHOUSE_CONTAINER_NAME:-langfuse-clickhouse}
|
||||
container_name: clickhouse
|
||||
hostname: clickhouse
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: clickhouse
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- ${HOST_IP:-127.0.0.1}:${CLICKHOUSE_HTTP_PORT:-8123}:8123
|
||||
- ${HOST_IP:-127.0.0.1}:${CLICKHOUSE_NATIVE_PORT:-9000}:9000
|
||||
- "8123:8123"
|
||||
- "9000:9000"
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- default
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
container_name: ${MINIO_CONTAINER_NAME:-langfuse-minio}
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-miniosecret}
|
||||
MINIO_ACCESS_KEY: minio
|
||||
MINIO_SECRET_KEY: miniosecret
|
||||
ports:
|
||||
- ${HOST_IP:-127.0.0.1}:${MINIO_API_PORT:-9090}:9000
|
||||
- ${HOST_IP:-127.0.0.1}:${MINIO_CONSOLE_PORT:-9091}:9001
|
||||
- "9090:9000"
|
||||
- "9091:9001"
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
@@ -40,24 +37,17 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
networks:
|
||||
- default
|
||||
|
||||
redis:
|
||||
image: docker.io/redis:7.2.4
|
||||
container_name: ${REDIS_CONTAINER_NAME:-langfuse-redis}
|
||||
image: redis:7.2.4
|
||||
restart: always
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
--maxmemory-policy noeviction
|
||||
ports:
|
||||
- ${HOST_IP:-127.0.0.1}:${REDIS_HOST_PORT:-6379}:6379
|
||||
networks:
|
||||
- default
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: docker.io/postgres:${POSTGRES_VERSION:-17}
|
||||
container_name: ${POSTGRES_CONTAINER_NAME:-langfuse-postgres}
|
||||
image: postgres:${POSTGRES_VERSION:-latest}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
@@ -66,33 +56,20 @@ services:
|
||||
retries: 10
|
||||
command: ["postgres", "-c", "log_statement=all"]
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-postgres}
|
||||
- TZ=UTC
|
||||
- PGTZ=UTC
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=postgres
|
||||
ports:
|
||||
- ${HOST_IP:-127.0.0.1}:${POSTGRES_HOST_PORT:-5432}:5432
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- default
|
||||
|
||||
volumes:
|
||||
langfuse_postgres_data:
|
||||
name: ${POSTGRES_VOLUME_NAME:-langfuse_postgres_data}
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
name: ${CLICKHOUSE_DATA_VOLUME_NAME:-langfuse_clickhouse_data}
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
name: ${CLICKHOUSE_LOGS_VOLUME_NAME:-langfuse_clickhouse_logs}
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
name: ${MINIO_VOLUME_NAME:-langfuse_minio_data}
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${DOCKER_NETWORK_NAME:-langfuse-network}
|
||||
driver: bridge
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
services:
|
||||
langfuse-worker:
|
||||
image: langfuse/langfuse-worker:latest
|
||||
depends_on: &langfuse-depends-on
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3030:3030"
|
||||
environment: &langfuse-worker-env
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||
SALT: "mysalt"
|
||||
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
|
||||
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_INGESTION_PROCESSING:-true}
|
||||
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING:-true}
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE: ${LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE:-true}
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
|
||||
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
|
||||
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: ${LANGFUSE_S3_EVENT_UPLOAD_ENABLED:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED: ${LANGFUSE_S3_MEDIA_UPLOAD_ENABLED:-true}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-us-east-1}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
|
||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
|
||||
|
||||
langfuse-web:
|
||||
image: langfuse/langfuse:latest
|
||||
depends_on: *langfuse-depends-on
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
<<: *langfuse-worker-env
|
||||
NEXTAUTH_URL: http://localhost:3000
|
||||
NEXTAUTH_SECRET: mysecret
|
||||
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
|
||||
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
|
||||
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server
|
||||
user: "101:101"
|
||||
container_name: clickhouse
|
||||
hostname: clickhouse
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: clickhouse
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- "8123:8123"
|
||||
- "9000:9000"
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: miniosecret
|
||||
ports:
|
||||
- "9090:9000"
|
||||
- "9091:9001"
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
restart: always
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
postgres:
|
||||
image: postgres:${POSTGRES_VERSION:-latest}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
langfuse_postgres_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
driver: local
|
||||
+29
-155
@@ -1,149 +1,31 @@
|
||||
# Make sure to update the credential placeholders with your own secrets.
|
||||
# We mark them with # CHANGEME in the file below.
|
||||
# In addition, we recommend to restrict inbound traffic on the host to langfuse-web (port 3000) and minio (port 9090) only.
|
||||
# All other components are bound to localhost (127.0.0.1) to only accept connections from the local machine.
|
||||
# External connections from other machines will not be able to reach these services directly.
|
||||
services:
|
||||
langfuse-worker:
|
||||
image: docker.io/langfuse/langfuse-worker:3
|
||||
restart: always
|
||||
depends_on: &langfuse-depends-on
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
langfuse-server:
|
||||
image: langfuse/langfuse:2
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 127.0.0.1:3030:3030
|
||||
environment: &langfuse-worker-env
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/postgres} # CHANGEME
|
||||
SALT: ${SALT:-mysalt} # CHANGEME
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -hex 32`
|
||||
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
|
||||
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
|
||||
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME
|
||||
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
|
||||
LANGFUSE_USE_AZURE_BLOB: ${LANGFUSE_USE_AZURE_BLOB:-false}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9090}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENABLED: ${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false}
|
||||
LANGFUSE_S3_BATCH_EXPORT_BUCKET: ${LANGFUSE_S3_BATCH_EXPORT_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX: ${LANGFUSE_S3_BATCH_EXPORT_PREFIX:-exports/}
|
||||
LANGFUSE_S3_BATCH_EXPORT_REGION: ${LANGFUSE_S3_BATCH_EXPORT_REGION:-auto}
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: ${LANGFUSE_S3_BATCH_EXPORT_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: ${LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT:-http://localhost:9090}
|
||||
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: ${LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: ${LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: ${LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_INGESTION_QUEUE_DELAY_MS: ${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-}
|
||||
LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS: ${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-}
|
||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_AUTH: ${REDIS_AUTH:-myredissecret} # CHANGEME
|
||||
REDIS_TLS_ENABLED: ${REDIS_TLS_ENABLED:-false}
|
||||
REDIS_TLS_CA: ${REDIS_TLS_CA:-/certs/ca.crt}
|
||||
REDIS_TLS_CERT: ${REDIS_TLS_CERT:-/certs/redis.crt}
|
||||
REDIS_TLS_KEY: ${REDIS_TLS_KEY:-/certs/redis.key}
|
||||
EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-}
|
||||
SMTP_CONNECTION_URL: ${SMTP_CONNECTION_URL:-}
|
||||
|
||||
langfuse-web:
|
||||
image: docker.io/langfuse/langfuse:3
|
||||
restart: always
|
||||
depends_on: *langfuse-depends-on
|
||||
ports:
|
||||
- 3000:3000
|
||||
- "3000:3000"
|
||||
environment:
|
||||
<<: *langfuse-worker-env
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-mysecret} # CHANGEME
|
||||
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
|
||||
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
|
||||
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
|
||||
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
|
||||
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
|
||||
clickhouse:
|
||||
image: docker.io/clickhouse/clickhouse-server
|
||||
restart: always
|
||||
user: "101:101"
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- 127.0.0.1:8123:8123
|
||||
- 127.0.0.1:9000:9000
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 1s
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
restart: always
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-miniosecret} # CHANGEME
|
||||
ports:
|
||||
- 9090:9000
|
||||
- 127.0.0.1:9091:9001
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
|
||||
redis:
|
||||
image: docker.io/redis:7
|
||||
restart: always
|
||||
# CHANGEME: row below to secure redis password
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
--maxmemory-policy noeviction
|
||||
ports:
|
||||
- 127.0.0.1:6379:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
postgres:
|
||||
image: docker.io/postgres:${POSTGRES_VERSION:-17}
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
@@ -151,22 +33,14 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} # CHANGEME
|
||||
POSTGRES_DB: ${POSTGRES_DB:-postgres}
|
||||
TZ: UTC
|
||||
PGTZ: UTC
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=postgres
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
- database_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
langfuse_postgres_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
database_data:
|
||||
driver: local
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["@repo/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
+11
-25
@@ -1,36 +1,24 @@
|
||||
START NOTE
|
||||
Langfuse is an open core project. Langfuse's core
|
||||
is permissively licensed (MIT license).
|
||||
Certain parts of the periphery of Langfuse are commercially
|
||||
licensed and governed by this Enterprise License.
|
||||
For the avoidance of doubt, this license does not apply
|
||||
to the core of Langfuse as it is defined in its license in
|
||||
the directory "/LICENSE" (as opposed to this file "ee/LICENSE")
|
||||
which can be used and run without infringing the below license
|
||||
and its licensed materials.
|
||||
END NOTE
|
||||
Langfuse Enterprise License (the “Enterprise License”)
|
||||
Copyright (c) 2024 Langfuse GmbH ('Langfuse')
|
||||
|
||||
START OF LICENSE
|
||||
Langfuse Enterprise license (the “Enterprise License” or "EE license")
|
||||
|
||||
Copyright (c) 2023-2025 Langfuse GmbH
|
||||
|
||||
With regard to the Langfuse Enterprise Software:
|
||||
With regard to the Langfuse Software:
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the applicable Langfuse Terms of Service, available
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Langfuse Terms of Service, available
|
||||
at https://langfuse.com/terms (the “Enterprise Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Langfuse,
|
||||
and otherwise have a valid Langfuse Enterprise License.
|
||||
and otherwise have a valid Langfuse Enterprise license.
|
||||
|
||||
Subject to the foregoing sentence, you are free to
|
||||
modify this Software and publish patches to the Software. You agree that Langfuse
|
||||
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||
to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Langfuse Enterprise License. Notwithstanding the foregoing, you may copy and modify
|
||||
exploited with a valid Langfuse Enterprise license.
|
||||
|
||||
Notwithstanding the foregoing, you may copy and modify
|
||||
the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Langfuse GmbH and/or its licensors (as applicable) retain
|
||||
subscription. You agree that Langfuse and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
@@ -45,7 +33,5 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Langfuse Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
|
||||
END OF LICENSE
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import baseConfig from "@repo/eslint-config";
|
||||
|
||||
export default [...baseConfig];
|
||||
+19
-16
@@ -15,37 +15,40 @@
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "24"
|
||||
"node": "20"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:check": "tsc",
|
||||
"typecheck": "dotenv -e ../../.env -- tsgo --noEmit --skipLibCheck --incremental --tsBuildInfoFile .tsbuildinfo",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --cache --cache-location dist/.eslintcache --max-warnings 0",
|
||||
"lint:fix": "eslint . --cache --cache-location dist/.eslintcache --fix"
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"axios": "^1.7.7",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"next": "15.5.10",
|
||||
"next-auth": "^4.24.13",
|
||||
"zod": "^3.25.62"
|
||||
"next": "^14.2.21",
|
||||
"next-auth": "^4.24.11",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^24.3.0",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260122.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"@types/node": "^20.11.29",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"prettier": "^3.6.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"jsonpath-plus": "10.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -1,9 +1,12 @@
|
||||
import { z } from "zod/v4";
|
||||
import { removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
import { env as sharedEnv, removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
|
||||
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
export const env = {
|
||||
...sharedEnv,
|
||||
...EnvSchema.parse(removeEmptyEnvVariables(process.env)),
|
||||
};
|
||||
|
||||
+2
-2
@@ -3,10 +3,10 @@
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext",
|
||||
"lib": ["es2023"],
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"types": ["node"],
|
||||
"target": "es2024",
|
||||
"target": "ES2020",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["."],
|
||||
|
||||
@@ -14,18 +14,13 @@ types:
|
||||
CreateScoreRequest:
|
||||
properties:
|
||||
id: optional<string>
|
||||
traceId: optional<string>
|
||||
sessionId: optional<string>
|
||||
observationId: optional<string>
|
||||
datasetRunId: optional<string>
|
||||
name:
|
||||
type: string
|
||||
docs: The name of the score. Always overrides "output" for correction scores.
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
observationId: optional<string>
|
||||
comment: optional<string>
|
||||
metadata: optional<unknown>
|
||||
dataType:
|
||||
type: optional<ScoreDataType>
|
||||
docs: When set, must match the score value's type. If not set, will be inferred from the score value or config
|
||||
@@ -69,25 +64,71 @@ types:
|
||||
dataType: "BOOLEAN"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "contextrelevant"
|
||||
value: "not relevant"
|
||||
sessionId: "abyt-1234-5678-80ab"
|
||||
- value:
|
||||
name: "hallucination"
|
||||
value: 0
|
||||
datasetRunId: "7891-5678-90ab-hijk"
|
||||
|
||||
BaseScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
NumericScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
BooleanScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
|
||||
CategoricalScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: optional<double>
|
||||
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
NUMERIC:
|
||||
type: NumericScore
|
||||
docs: "Score with NUMERIC data type"
|
||||
CATEGORICAL:
|
||||
type: CategoricalScore
|
||||
docs: "Score with CATEGORICAL data type"
|
||||
BOOLEAN:
|
||||
type: BooleanScore
|
||||
docs: "Score with BOOLEAN data type"
|
||||
ScoreSource:
|
||||
enum:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- EVAL
|
||||
ScoreDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- CATEGORICAL
|
||||
- BOOLEAN
|
||||
- CORRECTION
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- double
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. For corrections pass value as string and set data type to CORRECTION
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
|
||||
@@ -12,7 +12,7 @@ groups:
|
||||
# namespaceExport: Langfuse
|
||||
# allowCustomFetcher: true
|
||||
- name: fernapi/fern-openapi
|
||||
version: 0.1.7
|
||||
version: 0.0.26
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/public/generated/api-client
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
name: langfuse-organizations
|
||||
docs: |
|
||||
## General Notes
|
||||
|
||||
This admin API is only available on self-hosted instances, not on Langfuse Cloud.
|
||||
An administrator must set the `ADMIN_API_KEY` environment variable to use this API.
|
||||
|
||||
## Authentication
|
||||
|
||||
Authenticate with the API by setting the Authorization header to `Bearer $ADMIN_API_KEY` where
|
||||
`$ADMIN_API_KEY` is the API key set via the container environment.
|
||||
|
||||
## Exports
|
||||
|
||||
- OpenAPI spec: https://cloud.langfuse.com/generated/organizations-api/openapi.yml
|
||||
- Postman collection: https://cloud.langfuse.com/generated/organizations-postman/collection.json
|
||||
|
||||
## Looking for the Langfuse API?
|
||||
|
||||
Are you looking for the full Langfuse API?
|
||||
Check out our [API reference](https://api.reference.langfuse.com/#overview).
|
||||
|
||||
error-discrimination:
|
||||
strategy: status-code
|
||||
auth: bearer
|
||||
imports:
|
||||
commons: commons.yml
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.AccessDeniedError
|
||||
- commons.MethodNotAllowedError
|
||||
@@ -1,13 +0,0 @@
|
||||
errors:
|
||||
Error:
|
||||
status-code: 400
|
||||
type: string
|
||||
UnauthorizedError:
|
||||
status-code: 401
|
||||
type: string
|
||||
AccessDeniedError:
|
||||
status-code: 403
|
||||
type: string
|
||||
MethodNotAllowedError:
|
||||
status-code: 405
|
||||
type: string
|
||||
@@ -1,182 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/admin
|
||||
endpoints:
|
||||
getAll:
|
||||
docs: Get all organizations
|
||||
method: GET
|
||||
path: /organizations
|
||||
response: Organizations
|
||||
errors:
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
create:
|
||||
docs: Create a new organization
|
||||
method: POST
|
||||
path: /organizations
|
||||
request:
|
||||
name: CreateOrganizationRequest
|
||||
body:
|
||||
properties:
|
||||
name: string
|
||||
metadata:
|
||||
type: optional<map<string, unknown>>
|
||||
docs: Optional metadata for the organization
|
||||
response: Organization
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
getById:
|
||||
docs: Get organization by ID
|
||||
method: GET
|
||||
path: /organizations/{organizationId}
|
||||
path-parameters:
|
||||
organizationId: string
|
||||
response: Organization
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
update:
|
||||
docs: Update an organization
|
||||
method: PUT
|
||||
path: /organizations/{organizationId}
|
||||
path-parameters:
|
||||
organizationId: string
|
||||
request:
|
||||
name: UpdateOrganizationRequest
|
||||
body:
|
||||
properties:
|
||||
name: string
|
||||
metadata:
|
||||
type: optional<map<string, unknown>>
|
||||
docs: Optional metadata for the organization
|
||||
response: Organization
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
delete:
|
||||
docs: Delete an organization
|
||||
method: DELETE
|
||||
path: /organizations/{organizationId}
|
||||
path-parameters:
|
||||
organizationId: string
|
||||
response: DeleteOrganizationResponse
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
# API Key endpoints
|
||||
getApiKeys:
|
||||
docs: Get all API keys for an organization
|
||||
method: GET
|
||||
path: /organizations/{organizationId}/apiKeys
|
||||
path-parameters:
|
||||
organizationId: string
|
||||
response: ApiKeyList
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
createApiKey:
|
||||
docs: Create a new API key for an organization
|
||||
method: POST
|
||||
path: /organizations/{organizationId}/apiKeys
|
||||
path-parameters:
|
||||
organizationId: string
|
||||
request:
|
||||
name: CreateApiKeyRequest
|
||||
body:
|
||||
properties:
|
||||
note:
|
||||
type: optional<string>
|
||||
docs: Optional note for the API key
|
||||
response: ApiKeyResponse
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
deleteApiKey:
|
||||
docs: Delete an API key for an organization
|
||||
method: DELETE
|
||||
path: /organizations/{organizationId}/apiKeys/{apiKeyId}
|
||||
path-parameters:
|
||||
organizationId: string
|
||||
apiKeyId: string
|
||||
response: DeleteOrganizationResponse
|
||||
errors:
|
||||
- commons.Error
|
||||
- commons.UnauthorizedError
|
||||
- commons.MethodNotAllowedError
|
||||
|
||||
types:
|
||||
Organizations:
|
||||
docs: List of organizations
|
||||
properties:
|
||||
organizations: list<Organization>
|
||||
|
||||
Organization:
|
||||
docs: Organization object
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
createdAt: datetime
|
||||
metadata:
|
||||
type: map<string, unknown>
|
||||
docs: Metadata for the organization
|
||||
projects: list<OrganizationProject>
|
||||
|
||||
OrganizationProject:
|
||||
docs: Project belonging to an organization
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
metadata:
|
||||
type: optional<map<string, unknown>>
|
||||
docs: Metadata for the project
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
|
||||
DeleteOrganizationResponse:
|
||||
docs: Response for successful organization deletion
|
||||
properties:
|
||||
success: boolean
|
||||
|
||||
ApiKeyList:
|
||||
docs: List of API keys for an organization
|
||||
properties:
|
||||
apiKeys: list<ApiKeySummary>
|
||||
|
||||
ApiKeySummary:
|
||||
docs: Summary of an API key
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
expiresAt: optional<datetime>
|
||||
lastUsedAt: optional<datetime>
|
||||
note: optional<string>
|
||||
publicKey: string
|
||||
displaySecretKey: string
|
||||
|
||||
ApiKeyResponse:
|
||||
docs: Response for API key creation
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
publicKey: string
|
||||
secretKey: string
|
||||
displaySecretKey: string
|
||||
note: optional<string>
|
||||
@@ -1,17 +0,0 @@
|
||||
default-group: local
|
||||
groups:
|
||||
local:
|
||||
generators:
|
||||
- name: fernapi/fern-openapi
|
||||
version: 0.1.7
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/public/generated/organizations-api
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-postman
|
||||
version: 0.0.45
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/public/generated/organizations-postman
|
||||
@@ -1,215 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
listQueues:
|
||||
docs: Get all annotation queues
|
||||
method: GET
|
||||
path: /annotation-queues
|
||||
request:
|
||||
name: GetAnnotationQueuesRequest
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
response: PaginatedAnnotationQueues
|
||||
|
||||
createQueue:
|
||||
docs: Create an annotation queue
|
||||
method: POST
|
||||
path: /annotation-queues
|
||||
request: CreateAnnotationQueueRequest
|
||||
response: AnnotationQueue
|
||||
|
||||
getQueue:
|
||||
docs: Get an annotation queue by ID
|
||||
method: GET
|
||||
path: /annotation-queues/{queueId}
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
response: AnnotationQueue
|
||||
|
||||
listQueueItems:
|
||||
docs: Get items for a specific annotation queue
|
||||
method: GET
|
||||
path: /annotation-queues/{queueId}/items
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
request:
|
||||
name: GetAnnotationQueueItemsRequest
|
||||
query-parameters:
|
||||
status:
|
||||
type: optional<AnnotationQueueStatus>
|
||||
docs: Filter by status
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
response: PaginatedAnnotationQueueItems
|
||||
|
||||
getQueueItem:
|
||||
docs: Get a specific item from an annotation queue
|
||||
method: GET
|
||||
path: /annotation-queues/{queueId}/items/{itemId}
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
itemId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue item
|
||||
response: AnnotationQueueItem
|
||||
|
||||
createQueueItem:
|
||||
docs: Add an item to an annotation queue
|
||||
method: POST
|
||||
path: /annotation-queues/{queueId}/items
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
request: CreateAnnotationQueueItemRequest
|
||||
response: AnnotationQueueItem
|
||||
|
||||
updateQueueItem:
|
||||
docs: Update an annotation queue item
|
||||
method: PATCH
|
||||
path: /annotation-queues/{queueId}/items/{itemId}
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
itemId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue item
|
||||
request: UpdateAnnotationQueueItemRequest
|
||||
response: AnnotationQueueItem
|
||||
|
||||
deleteQueueItem:
|
||||
docs: Remove an item from an annotation queue
|
||||
method: DELETE
|
||||
path: /annotation-queues/{queueId}/items/{itemId}
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
itemId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue item
|
||||
response: DeleteAnnotationQueueItemResponse
|
||||
|
||||
createQueueAssignment:
|
||||
docs: Create an assignment for a user to an annotation queue
|
||||
method: POST
|
||||
path: /annotation-queues/{queueId}/assignments
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
request: AnnotationQueueAssignmentRequest
|
||||
response: CreateAnnotationQueueAssignmentResponse
|
||||
|
||||
deleteQueueAssignment:
|
||||
docs: Delete an assignment for a user to an annotation queue
|
||||
method: DELETE
|
||||
path: /annotation-queues/{queueId}/assignments
|
||||
path-parameters:
|
||||
queueId:
|
||||
type: string
|
||||
docs: The unique identifier of the annotation queue
|
||||
request: AnnotationQueueAssignmentRequest
|
||||
response: DeleteAnnotationQueueAssignmentResponse
|
||||
|
||||
types:
|
||||
AnnotationQueueStatus:
|
||||
enum:
|
||||
- PENDING
|
||||
- COMPLETED
|
||||
|
||||
AnnotationQueueObjectType:
|
||||
enum:
|
||||
- TRACE
|
||||
- OBSERVATION
|
||||
- SESSION
|
||||
|
||||
AnnotationQueue:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
description: optional<string>
|
||||
scoreConfigIds: list<string>
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
|
||||
AnnotationQueueItem:
|
||||
properties:
|
||||
id: string
|
||||
queueId: string
|
||||
objectId: string
|
||||
objectType: AnnotationQueueObjectType
|
||||
status: AnnotationQueueStatus
|
||||
completedAt: optional<datetime>
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
|
||||
PaginatedAnnotationQueues:
|
||||
properties:
|
||||
data: list<AnnotationQueue>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
PaginatedAnnotationQueueItems:
|
||||
properties:
|
||||
data: list<AnnotationQueueItem>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
CreateAnnotationQueueRequest:
|
||||
properties:
|
||||
name: string
|
||||
description: optional<string>
|
||||
scoreConfigIds: list<string>
|
||||
|
||||
CreateAnnotationQueueItemRequest:
|
||||
properties:
|
||||
objectId: string
|
||||
objectType: AnnotationQueueObjectType
|
||||
status:
|
||||
type: optional<AnnotationQueueStatus>
|
||||
docs: Defaults to PENDING for new queue items
|
||||
|
||||
UpdateAnnotationQueueItemRequest:
|
||||
properties:
|
||||
status:
|
||||
type: optional<AnnotationQueueStatus>
|
||||
|
||||
DeleteAnnotationQueueItemResponse:
|
||||
properties:
|
||||
success: boolean
|
||||
message: string
|
||||
|
||||
AnnotationQueueAssignmentRequest:
|
||||
properties:
|
||||
userId: string
|
||||
|
||||
DeleteAnnotationQueueAssignmentResponse:
|
||||
properties:
|
||||
success: boolean
|
||||
|
||||
CreateAnnotationQueueAssignmentResponse:
|
||||
properties:
|
||||
userId: string
|
||||
queueId: string
|
||||
projectId: string
|
||||
@@ -1,119 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public/integrations/blob-storage
|
||||
endpoints:
|
||||
getBlobStorageIntegrations:
|
||||
docs: Get all blob storage integrations for the organization (requires organization-scoped API key)
|
||||
method: GET
|
||||
path: ""
|
||||
response: BlobStorageIntegrationsResponse
|
||||
|
||||
upsertBlobStorageIntegration:
|
||||
docs: Create or update a blob storage integration for a specific project (requires organization-scoped API key). The configuration is validated by performing a test upload to the bucket.
|
||||
method: PUT
|
||||
path: ""
|
||||
request: CreateBlobStorageIntegrationRequest
|
||||
response: BlobStorageIntegrationResponse
|
||||
|
||||
deleteBlobStorageIntegration:
|
||||
docs: Delete a blob storage integration by ID (requires organization-scoped API key)
|
||||
method: DELETE
|
||||
path: "/{id}"
|
||||
path-parameters:
|
||||
id: string
|
||||
response: BlobStorageIntegrationDeletionResponse
|
||||
|
||||
types:
|
||||
BlobStorageIntegrationType:
|
||||
enum:
|
||||
- S3
|
||||
- S3_COMPATIBLE
|
||||
- AZURE_BLOB_STORAGE
|
||||
|
||||
BlobStorageIntegrationFileType:
|
||||
enum:
|
||||
- JSON
|
||||
- CSV
|
||||
- JSONL
|
||||
|
||||
BlobStorageExportMode:
|
||||
enum:
|
||||
- FULL_HISTORY
|
||||
- FROM_TODAY
|
||||
- FROM_CUSTOM_DATE
|
||||
|
||||
BlobStorageExportFrequency:
|
||||
enum:
|
||||
- hourly
|
||||
- daily
|
||||
- weekly
|
||||
|
||||
CreateBlobStorageIntegrationRequest:
|
||||
properties:
|
||||
projectId:
|
||||
type: string
|
||||
docs: ID of the project in which to configure the blob storage integration
|
||||
type: BlobStorageIntegrationType
|
||||
bucketName:
|
||||
type: string
|
||||
docs: Name of the storage bucket
|
||||
endpoint:
|
||||
type: optional<string>
|
||||
docs: Custom endpoint URL (required for S3_COMPATIBLE type)
|
||||
region:
|
||||
type: string
|
||||
docs: Storage region
|
||||
accessKeyId:
|
||||
type: optional<string>
|
||||
docs: Access key ID for authentication
|
||||
secretAccessKey:
|
||||
type: optional<string>
|
||||
docs: Secret access key for authentication (will be encrypted when stored)
|
||||
prefix:
|
||||
type: optional<string>
|
||||
docs: Path prefix for exported files (must end with forward slash if provided)
|
||||
exportFrequency: BlobStorageExportFrequency
|
||||
enabled:
|
||||
type: boolean
|
||||
docs: Whether the integration is active
|
||||
forcePathStyle:
|
||||
type: boolean
|
||||
docs: Use path-style URLs for S3 requests
|
||||
fileType: BlobStorageIntegrationFileType
|
||||
exportMode: BlobStorageExportMode
|
||||
exportStartDate:
|
||||
type: optional<datetime>
|
||||
docs: Custom start date for exports (required when exportMode is FROM_CUSTOM_DATE)
|
||||
|
||||
BlobStorageIntegrationResponse:
|
||||
properties:
|
||||
id: string
|
||||
projectId: string
|
||||
type: BlobStorageIntegrationType
|
||||
bucketName: string
|
||||
endpoint: optional<string>
|
||||
region: string
|
||||
accessKeyId: optional<string>
|
||||
prefix: string
|
||||
exportFrequency: BlobStorageExportFrequency
|
||||
enabled: boolean
|
||||
forcePathStyle: boolean
|
||||
fileType: BlobStorageIntegrationFileType
|
||||
exportMode: BlobStorageExportMode
|
||||
exportStartDate: optional<datetime>
|
||||
nextSyncAt: optional<datetime>
|
||||
lastSyncAt: optional<datetime>
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
|
||||
BlobStorageIntegrationsResponse:
|
||||
properties:
|
||||
data: list<BlobStorageIntegrationResponse>
|
||||
|
||||
BlobStorageIntegrationDeletionResponse:
|
||||
properties:
|
||||
message: string
|
||||
@@ -58,7 +58,7 @@ types:
|
||||
docs: The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown.
|
||||
content:
|
||||
type: string
|
||||
docs: The content of the comment. May include markdown. Currently limited to 5000 characters.
|
||||
docs: The content of the comment. May include markdown. Currently limited to 500 characters.
|
||||
authorUserId:
|
||||
type: optional<string>
|
||||
docs: The id of the user who created the comment.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
types:
|
||||
# Objects
|
||||
# Source: web/src/features/public-api/types/traces.ts - APITrace, APIExtendedTrace, GetTraceV1Response
|
||||
Trace:
|
||||
properties:
|
||||
id:
|
||||
@@ -10,7 +9,7 @@ types:
|
||||
type: datetime
|
||||
docs: The timestamp when the trace was created
|
||||
name:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The name of the trace
|
||||
input:
|
||||
type: optional<unknown>
|
||||
@@ -19,29 +18,26 @@ types:
|
||||
type: optional<unknown>
|
||||
docs: The output data of the trace. Can be any JSON.
|
||||
sessionId:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The session identifier associated with the trace
|
||||
release:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The release version of the application when the trace was created
|
||||
version:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The version of the trace
|
||||
userId:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The user identifier associated with the trace
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
docs: The metadata associated with the trace. Can be any JSON.
|
||||
tags:
|
||||
type: list<string>
|
||||
docs: The tags associated with the trace.
|
||||
type: optional<list<string>>
|
||||
docs: The tags associated with the trace. Can be an array of strings or null.
|
||||
public:
|
||||
type: boolean
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
environment:
|
||||
type: string
|
||||
docs: The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
TraceWithDetails: # GET /traces
|
||||
extends: Trace
|
||||
properties:
|
||||
@@ -49,16 +45,16 @@ types:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: optional<nullable<double>>
|
||||
type: double
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: optional<nullable<double>>
|
||||
type: double
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: optional<nullable<list<string>>>
|
||||
type: list<string>
|
||||
docs: List of observation ids
|
||||
scores:
|
||||
type: optional<nullable<list<string>>>
|
||||
type: list<string>
|
||||
docs: List of score ids
|
||||
TraceWithFullDetails: # GET traces/[traceID]
|
||||
extends: Trace
|
||||
@@ -67,149 +63,133 @@ types:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: optional<nullable<double>>
|
||||
type: double
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: optional<nullable<double>>
|
||||
type: double
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: list<ObservationsView>
|
||||
docs: List of observations
|
||||
scores:
|
||||
type: list<ScoreV1>
|
||||
type: list<Score>
|
||||
docs: List of scores
|
||||
# Source: web/src/features/public-api/types/sessions.ts - APISession
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
projectId: string
|
||||
environment:
|
||||
type: string
|
||||
docs: The environment from which this session originated.
|
||||
SessionWithTraces:
|
||||
extends: Session
|
||||
properties:
|
||||
traces: list<Trace>
|
||||
# Source: web/src/features/public-api/types/observations.ts - APIObservation
|
||||
Observation:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
docs: The unique identifier of the observation
|
||||
traceId:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The trace ID associated with the observation
|
||||
type:
|
||||
type: string
|
||||
docs: The type of the observation
|
||||
name:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The name of the observation
|
||||
startTime:
|
||||
type: datetime
|
||||
docs: The start time of the observation
|
||||
endTime:
|
||||
type: nullable<datetime>
|
||||
type: optional<datetime>
|
||||
docs: The end time of the observation.
|
||||
completionStartTime:
|
||||
type: nullable<datetime>
|
||||
type: optional<datetime>
|
||||
docs: The completion start time of the observation
|
||||
model:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The model used for the observation
|
||||
modelParameters:
|
||||
type: unknown
|
||||
type: optional<map<string, MapValue>>
|
||||
docs: The parameters of the model used for the observation
|
||||
input:
|
||||
type: unknown
|
||||
type: optional<unknown>
|
||||
docs: The input data of the observation
|
||||
version:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The version of the observation
|
||||
metadata:
|
||||
type: unknown
|
||||
type: optional<unknown>
|
||||
docs: Additional metadata of the observation
|
||||
output:
|
||||
type: unknown
|
||||
type: optional<unknown>
|
||||
docs: The output data of the observation
|
||||
usage:
|
||||
type: Usage
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation
|
||||
type: optional<Usage>
|
||||
docs: The usage data of the observation
|
||||
level:
|
||||
type: ObservationLevel
|
||||
docs: The level of the observation
|
||||
statusMessage:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The status message of the observation
|
||||
parentObservationId:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The parent observation ID
|
||||
promptId:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The prompt ID associated with the observation
|
||||
usageDetails:
|
||||
type: map<string, integer>
|
||||
docs: The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested.
|
||||
costDetails:
|
||||
type: map<string, double>
|
||||
docs: The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested.
|
||||
environment:
|
||||
type: string
|
||||
docs: The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
|
||||
ObservationsView:
|
||||
extends: Observation
|
||||
properties:
|
||||
promptName:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The name of the prompt associated with the observation
|
||||
promptVersion:
|
||||
type: nullable<integer>
|
||||
type: optional<integer>
|
||||
docs: The version of the prompt associated with the observation
|
||||
modelId:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: The unique identifier of the model
|
||||
inputPrice:
|
||||
type: nullable<double>
|
||||
type: optional<double>
|
||||
docs: The price of the input in USD
|
||||
outputPrice:
|
||||
type: nullable<double>
|
||||
type: optional<double>
|
||||
docs: The price of the output in USD.
|
||||
totalPrice:
|
||||
type: nullable<double>
|
||||
type: optional<double>
|
||||
docs: The total price in USD.
|
||||
calculatedInputCost:
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD
|
||||
type: optional<double>
|
||||
docs: The calculated cost of the input in USD
|
||||
calculatedOutputCost:
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD
|
||||
type: optional<double>
|
||||
docs: The calculated cost of the output in USD
|
||||
calculatedTotalCost:
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD
|
||||
type: optional<double>
|
||||
docs: The calculated total cost in USD
|
||||
latency:
|
||||
type: nullable<double>
|
||||
type: optional<double>
|
||||
docs: The latency in seconds.
|
||||
timeToFirstToken:
|
||||
type: nullable<double>
|
||||
type: optional<double>
|
||||
docs: The time to the first token in seconds
|
||||
|
||||
Usage:
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) Standard interface for usage and cost
|
||||
docs: Standard interface for usage and cost
|
||||
properties:
|
||||
input:
|
||||
docs: Number of input units (e.g. tokens)
|
||||
type: integer
|
||||
type: optional<integer>
|
||||
output:
|
||||
docs: Number of output units (e.g. tokens)
|
||||
type: integer
|
||||
type: optional<integer>
|
||||
total:
|
||||
docs: Defaults to input+output if not set
|
||||
type: integer
|
||||
unit:
|
||||
docs: Unit of measurement
|
||||
type: nullable<string>
|
||||
type: optional<integer>
|
||||
unit: optional<ModelUsageUnit>
|
||||
inputCost:
|
||||
docs: USD input cost
|
||||
type: optional<double>
|
||||
@@ -219,7 +199,6 @@ types:
|
||||
totalCost:
|
||||
docs: USD total cost, defaults to input+output
|
||||
type: optional<double>
|
||||
# Source: web/src/features/public-api/types/score-configs.ts - APIScoreConfig
|
||||
ScoreConfig:
|
||||
docs: Configuration for a score
|
||||
properties:
|
||||
@@ -228,133 +207,42 @@ types:
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
projectId: string
|
||||
dataType: ScoreConfigDataType
|
||||
dataType: ScoreDataType
|
||||
isArchived:
|
||||
type: boolean
|
||||
docs: Whether the score config is archived. Defaults to false
|
||||
minValue:
|
||||
type: optional<nullable<double>>
|
||||
type: optional<double>
|
||||
docs: Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞
|
||||
maxValue:
|
||||
type: optional<nullable<double>>
|
||||
type: optional<double>
|
||||
docs: Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞
|
||||
categories:
|
||||
type: optional<list<ConfigCategory>>
|
||||
docs: Configures custom categories for categorical scores
|
||||
description:
|
||||
type: optional<nullable<string>>
|
||||
docs: Description of the score config
|
||||
description: optional<string>
|
||||
ConfigCategory:
|
||||
properties:
|
||||
value: double
|
||||
label: string
|
||||
# Source: packages/shared/src/features/scores/interfaces/api/v1/schemas.ts - APIScoreSchemaV1
|
||||
BaseScoreV1:
|
||||
BaseScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
source: ScoreSource
|
||||
observationId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The observation ID associated with the score
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId:
|
||||
type: nullable<string>
|
||||
docs: The user ID of the author
|
||||
comment:
|
||||
type: nullable<string>
|
||||
docs: Comment on the score
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the score
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
queueId:
|
||||
type: nullable<string>
|
||||
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
|
||||
environment:
|
||||
type: string
|
||||
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
NumericScoreV1:
|
||||
extends: BaseScoreV1
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
BooleanScoreV1:
|
||||
extends: BaseScoreV1
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
|
||||
CategoricalScoreV1:
|
||||
extends: BaseScoreV1
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: Represents the numeric category mapping of the stringValue. If no config is linked, defaults to 0.
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
ScoreV1:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
NUMERIC:
|
||||
type: NumericScoreV1
|
||||
docs: "Score with NUMERIC data type"
|
||||
CATEGORICAL:
|
||||
type: CategoricalScoreV1
|
||||
docs: "Score with CATEGORICAL data type"
|
||||
BOOLEAN:
|
||||
type: BooleanScoreV1
|
||||
docs: "Score with BOOLEAN data type"
|
||||
|
||||
# Source: packages/shared/src/features/scores/interfaces/api/v2/schemas.ts - APIScoreSchemaV2
|
||||
BaseScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The trace ID associated with the score
|
||||
sessionId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The session ID associated with the score
|
||||
observationId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The observation ID associated with the score
|
||||
datasetRunId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The dataset run ID associated with the score
|
||||
name: string
|
||||
source: ScoreSource
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId:
|
||||
type: nullable<string>
|
||||
docs: The user ID of the author
|
||||
comment:
|
||||
type: nullable<string>
|
||||
docs: Comment on the score
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the score
|
||||
configId:
|
||||
type: nullable<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
queueId:
|
||||
type: nullable<string>
|
||||
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
|
||||
environment:
|
||||
type: string
|
||||
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
type: optional<string>
|
||||
docs: Reference an annotation queue on a score. Populated if the score was initially created in an annotation queue.
|
||||
NumericScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
@@ -374,20 +262,11 @@ types:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: Represents the numeric category mapping of the stringValue. If no config is linked, defaults to 0.
|
||||
type: optional<double>
|
||||
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
CorrectionScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Always 0 for correction scores.
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the correction content
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
@@ -400,9 +279,6 @@ types:
|
||||
BOOLEAN:
|
||||
type: BooleanScore
|
||||
docs: "Score with BOOLEAN data type"
|
||||
CORRECTION:
|
||||
type: CorrectionScore
|
||||
docs: "Score with CORRECTION data type"
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
@@ -411,7 +287,6 @@ types:
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
|
||||
# Source: web/src/features/public-api/types/comments.ts - APIComment
|
||||
Comment:
|
||||
properties:
|
||||
id: string
|
||||
@@ -421,55 +296,30 @@ types:
|
||||
objectType: CommentObjectType
|
||||
objectId: string
|
||||
content: string
|
||||
authorUserId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The user ID of the comment author
|
||||
authorUserId: optional<string>
|
||||
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDataset
|
||||
Dataset:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
description:
|
||||
type: nullable<string>
|
||||
docs: Description of the dataset
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the dataset
|
||||
inputSchema:
|
||||
type: nullable<unknown>
|
||||
docs: JSON Schema for validating dataset item inputs
|
||||
expectedOutputSchema:
|
||||
type: nullable<unknown>
|
||||
docs: JSON Schema for validating dataset item expected outputs
|
||||
description: optional<string>
|
||||
metadata: optional<unknown>
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetItem
|
||||
DatasetItem:
|
||||
properties:
|
||||
id: string
|
||||
status: DatasetStatus
|
||||
input:
|
||||
type: unknown
|
||||
docs: Input data for the dataset item
|
||||
expectedOutput:
|
||||
type: unknown
|
||||
docs: Expected output for the dataset item
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the dataset item
|
||||
sourceTraceId:
|
||||
type: nullable<string>
|
||||
docs: The trace ID that sourced this dataset item
|
||||
sourceObservationId:
|
||||
type: nullable<string>
|
||||
docs: The observation ID that sourced this dataset item
|
||||
input: optional<unknown>
|
||||
expectedOutput: optional<unknown>
|
||||
metadata: optional<unknown>
|
||||
sourceTraceId: optional<string>
|
||||
sourceObservationId: optional<string>
|
||||
datasetId: string
|
||||
datasetName: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRunItem
|
||||
DatasetRunItem:
|
||||
properties:
|
||||
id: string
|
||||
@@ -477,12 +327,9 @@ types:
|
||||
datasetRunName: string
|
||||
datasetItemId: string
|
||||
traceId: string
|
||||
observationId:
|
||||
type: nullable<string>
|
||||
docs: The observation ID associated with this run item
|
||||
observationId: optional<string>
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRun
|
||||
DatasetRun:
|
||||
properties:
|
||||
id:
|
||||
@@ -492,10 +339,10 @@ types:
|
||||
type: string
|
||||
docs: Name of the dataset run
|
||||
description:
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
docs: Description of the run
|
||||
metadata:
|
||||
type: unknown
|
||||
type: optional<unknown>
|
||||
docs: Metadata of the dataset run
|
||||
datasetId:
|
||||
type: string
|
||||
@@ -513,18 +360,8 @@ types:
|
||||
extends: DatasetRun
|
||||
properties:
|
||||
datasetRunItems: list<DatasetRunItem>
|
||||
# Source: web/src/features/public-api/types/models.ts - APIModelDefinition
|
||||
Model:
|
||||
docs: |
|
||||
Model definition used for transforming usage into USD cost and/or tokenization.
|
||||
|
||||
Models can have either simple flat pricing or tiered pricing:
|
||||
- Flat pricing: Single price per usage type (legacy, but still supported)
|
||||
- Tiered pricing: Multiple pricing tiers with conditional matching based on usage patterns
|
||||
|
||||
The pricing tiers approach is recommended for models with usage-based pricing variations.
|
||||
When using tiered pricing, the flat price fields (inputPrice, outputPrice, prices) are populated
|
||||
from the default tier for backward compatibility.
|
||||
docs: Model definition used for transforming usage into USD cost and/or tokenization.
|
||||
properties:
|
||||
id: string
|
||||
modelName:
|
||||
@@ -535,230 +372,27 @@ types:
|
||||
type: string
|
||||
startDate:
|
||||
docs: Apply only to generations which are newer than this ISO date.
|
||||
type: nullable<datetime>
|
||||
type: optional<date>
|
||||
unit:
|
||||
docs: Unit used by this model.
|
||||
type: optional<nullable<ModelUsageUnit>>
|
||||
type: ModelUsageUnit
|
||||
inputPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per input unit
|
||||
type: nullable<double>
|
||||
docs: Price (USD) per input unit
|
||||
type: optional<double>
|
||||
outputPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per output unit
|
||||
type: nullable<double>
|
||||
docs: Price (USD) per output unit
|
||||
type: optional<double>
|
||||
totalPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per total unit. Cannot be set if input or output price is set.
|
||||
type: nullable<double>
|
||||
docs: Price (USD) per total unit. Cannot be set if input or output price is set.
|
||||
type: optional<double>
|
||||
tokenizerId:
|
||||
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
|
||||
type: nullable<string>
|
||||
type: optional<string>
|
||||
tokenizerConfig:
|
||||
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
|
||||
type: unknown
|
||||
type: optional<unknown>
|
||||
isLangfuseManaged:
|
||||
type: boolean
|
||||
createdAt:
|
||||
docs: Timestamp when the model was created
|
||||
type: datetime
|
||||
prices:
|
||||
docs: |
|
||||
Deprecated. Use 'pricingTiers' instead for models with usage-based pricing variations.
|
||||
|
||||
This field shows prices by usage type from the default pricing tier. Maintained for backward compatibility.
|
||||
If the model uses tiered pricing, this field will be populated from the default tier's prices.
|
||||
type: map<string, ModelPrice>
|
||||
pricingTiers:
|
||||
docs: |
|
||||
Array of pricing tiers with conditional pricing based on usage thresholds.
|
||||
|
||||
Pricing tiers enable accurate cost tracking for models that charge different rates based on usage patterns
|
||||
(e.g., different rates for high-volume usage, large context windows, or cached tokens).
|
||||
|
||||
Each model must have exactly one default tier (isDefault=true, priority=0) that serves as a fallback.
|
||||
Additional conditional tiers can be defined with specific matching criteria.
|
||||
|
||||
If this array is empty, the model uses legacy flat pricing from the inputPrice/outputPrice/totalPrice fields.
|
||||
type: list<PricingTier>
|
||||
|
||||
ModelPrice:
|
||||
properties:
|
||||
price: double
|
||||
|
||||
PricingTierCondition:
|
||||
docs: |
|
||||
Condition for matching a pricing tier based on usage details. Used to implement tiered pricing models where costs vary based on usage thresholds.
|
||||
|
||||
How it works:
|
||||
1. The regex pattern matches against usage detail keys (e.g., "input_tokens", "input_cached")
|
||||
2. Values of all matching keys are summed together
|
||||
3. The sum is compared against the threshold value using the specified operator
|
||||
4. All conditions in a tier must be met (AND logic) for the tier to match
|
||||
|
||||
Common use cases:
|
||||
- Threshold-based pricing: Match when accumulated usage exceeds a certain amount
|
||||
- Usage-type-specific pricing: Different rates for cached vs non-cached tokens, or input vs output
|
||||
- Volume-based pricing: Different rates based on total request or token count
|
||||
properties:
|
||||
usageDetailPattern:
|
||||
docs: |
|
||||
Regex pattern to match against usage detail keys. All matching keys' values are summed for threshold comparison.
|
||||
|
||||
Examples:
|
||||
- "^input" matches "input", "input_tokens", "input_cached", etc.
|
||||
- "^(input|prompt)" matches both "input_tokens" and "prompt_tokens"
|
||||
- "_cache$" matches "input_cache", "output_cache", etc.
|
||||
|
||||
The pattern is case-insensitive by default. If no keys match, the sum is treated as zero.
|
||||
type: string
|
||||
operator:
|
||||
docs: |
|
||||
Comparison operator to apply between the summed value and the threshold.
|
||||
|
||||
- gt: greater than (sum > threshold)
|
||||
- gte: greater than or equal (sum >= threshold)
|
||||
- lt: less than (sum < threshold)
|
||||
- lte: less than or equal (sum <= threshold)
|
||||
- eq: equal (sum == threshold)
|
||||
- neq: not equal (sum != threshold)
|
||||
type: PricingTierOperator
|
||||
value:
|
||||
docs: Threshold value for comparison. For token-based pricing, this is typically the token count threshold (e.g., 200000 for a 200K token threshold).
|
||||
type: double
|
||||
caseSensitive:
|
||||
docs: Whether the regex pattern matching is case-sensitive. Default is false (case-insensitive matching).
|
||||
type: boolean
|
||||
|
||||
PricingTier:
|
||||
docs: |
|
||||
Pricing tier definition with conditional pricing based on usage thresholds.
|
||||
|
||||
Pricing tiers enable accurate cost tracking for LLM providers that charge different rates based on usage patterns.
|
||||
For example, some providers charge higher rates when context size exceeds certain thresholds.
|
||||
|
||||
How tier matching works:
|
||||
1. Tiers are evaluated in ascending priority order (priority 1 before priority 2, etc.)
|
||||
2. The first tier where ALL conditions match is selected
|
||||
3. If no conditional tiers match, the default tier is used as a fallback
|
||||
4. The default tier has priority 0 and no conditions
|
||||
|
||||
Why priorities matter:
|
||||
- Lower priority numbers are evaluated first, allowing you to define specific cases before general ones
|
||||
- Example: Priority 1 for "high usage" (>200K tokens), Priority 2 for "medium usage" (>100K tokens), Priority 0 for default
|
||||
- Without proper ordering, a less specific condition might match before a more specific one
|
||||
|
||||
Every model must have exactly one default tier to ensure cost calculation always succeeds.
|
||||
properties:
|
||||
id:
|
||||
docs: Unique identifier for the pricing tier
|
||||
type: string
|
||||
name:
|
||||
docs: |
|
||||
Name of the pricing tier for display and identification purposes.
|
||||
|
||||
Examples: "Standard", "High Volume Tier", "Large Context", "Extended Context Tier"
|
||||
type: string
|
||||
isDefault:
|
||||
docs: |
|
||||
Whether this is the default tier. Every model must have exactly one default tier with priority 0 and no conditions.
|
||||
|
||||
The default tier serves as a fallback when no conditional tiers match, ensuring cost calculation always succeeds.
|
||||
It typically represents the base pricing for standard usage patterns.
|
||||
type: boolean
|
||||
priority:
|
||||
docs: |
|
||||
Priority for tier matching evaluation. Lower numbers = higher priority (evaluated first).
|
||||
|
||||
The default tier must always have priority 0. Conditional tiers should have priority 1, 2, 3, etc.
|
||||
|
||||
Example ordering:
|
||||
- Priority 0: Default tier (no conditions, always matches as fallback)
|
||||
- Priority 1: High usage tier (e.g., >200K tokens)
|
||||
- Priority 2: Medium usage tier (e.g., >100K tokens)
|
||||
|
||||
This ensures more specific conditions are checked before general ones.
|
||||
type: integer
|
||||
conditions:
|
||||
docs: |
|
||||
Array of conditions that must ALL be met for this tier to match (AND logic).
|
||||
|
||||
The default tier must have an empty conditions array. Conditional tiers should have one or more conditions
|
||||
that define when this tier's pricing applies.
|
||||
|
||||
Multiple conditions enable complex matching scenarios (e.g., "high input tokens AND low output tokens").
|
||||
type: list<PricingTierCondition>
|
||||
prices:
|
||||
docs: |
|
||||
Prices (USD) by usage type for this tier.
|
||||
|
||||
Common usage types: "input", "output", "total", "request", "image"
|
||||
Prices are specified in USD per unit (e.g., per token, per request, per second).
|
||||
|
||||
Example: {"input": 0.000003, "output": 0.000015} means $3 per million input tokens and $15 per million output tokens.
|
||||
type: map<string, double>
|
||||
|
||||
PricingTierInput:
|
||||
docs: |
|
||||
Input schema for creating a pricing tier. The tier ID will be automatically generated server-side.
|
||||
|
||||
When creating a model with pricing tiers:
|
||||
- Exactly one tier must have isDefault=true (the fallback tier)
|
||||
- The default tier must have priority=0 and conditions=[]
|
||||
- All tier names and priorities must be unique within the model
|
||||
- Each tier must define at least one price
|
||||
|
||||
See PricingTier for detailed information about how tiers work and why they're useful.
|
||||
properties:
|
||||
name:
|
||||
docs: |
|
||||
Name of the pricing tier for display and identification purposes.
|
||||
|
||||
Must be unique within the model. Common patterns: "Standard", "High Volume Tier", "Extended Context"
|
||||
type: string
|
||||
isDefault:
|
||||
docs: |
|
||||
Whether this is the default tier. Exactly one tier per model must be marked as default.
|
||||
|
||||
Requirements for default tier:
|
||||
- Must have isDefault=true
|
||||
- Must have priority=0
|
||||
- Must have empty conditions array (conditions=[])
|
||||
|
||||
The default tier acts as a fallback when no conditional tiers match.
|
||||
type: boolean
|
||||
priority:
|
||||
docs: |
|
||||
Priority for tier matching evaluation. Lower numbers = higher priority (evaluated first).
|
||||
|
||||
Must be unique within the model. The default tier must have priority=0.
|
||||
Conditional tiers should use priority 1, 2, 3, etc. based on their specificity.
|
||||
type: integer
|
||||
conditions:
|
||||
docs: |
|
||||
Array of conditions that must ALL be met for this tier to match (AND logic).
|
||||
|
||||
The default tier must have an empty array (conditions=[]).
|
||||
Conditional tiers should define one or more conditions that specify when this tier's pricing applies.
|
||||
|
||||
Each condition specifies a regex pattern, operator, and threshold value for matching against usage details.
|
||||
type: list<PricingTierCondition>
|
||||
prices:
|
||||
docs: |
|
||||
Prices (USD) by usage type for this tier. At least one price must be defined.
|
||||
|
||||
Common usage types: "input", "output", "total", "request", "image"
|
||||
Prices are in USD per unit (e.g., per token).
|
||||
|
||||
Example: {"input": 0.000003, "output": 0.000015} represents $3 per million input tokens and $15 per million output tokens.
|
||||
type: map<string, double>
|
||||
|
||||
PricingTierOperator:
|
||||
docs: Comparison operators for pricing tier conditions
|
||||
enum:
|
||||
- gt
|
||||
- gte
|
||||
- lt
|
||||
- lte
|
||||
- eq
|
||||
- neq
|
||||
|
||||
# Utilities
|
||||
ModelUsageUnit:
|
||||
@@ -798,18 +432,11 @@ types:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- EVAL
|
||||
ScoreConfigDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
|
||||
ScoreDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
- CORRECTION
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user