Skip to content

Commit 20d66bb

Browse files
feat(observability): add Langfuse tracing + fix templates API
- Add Langfuse OpenTelemetry integration for AI observability - All 6 streamText/generateText calls now have experimental_telemetry - instrumentation.ts registers LangfuseSpanProcessor alongside Sentry - Sentry configured with skipOpenTelemetrySetup to avoid conflicts - Requires LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, LANGFUSE_BASEURL envs - Fix templates API returning empty for system templates - GET filtered by tenant_id which system templates don't have - Changed to: tenant_id = current OR tenant_id IS NULL - POST now includes tenant_id for user-created templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4126123 commit 20d66bb

11 files changed

Lines changed: 324 additions & 2 deletions

File tree

app/actions/exam-grading.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ Evaluate these free-text answers now:`
367367
prompt: aiPrompt,
368368
temperature: 0.3, // Lower temperature for more consistent grading
369369
topP: 0.8,
370+
experimental_telemetry: { isEnabled: true, functionId: 'exam-grading', metadata: { examId: String(params.examId), submissionId: String(params.submissionId) } },
370371
})
371372

372373
const text = result.text

app/api/chat/aristotle/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ export async function POST(req: Request) {
234234
model: AI_MODELS.aristotle,
235235
system: systemPrompt,
236236
messages: await convertToModelMessages(messages),
237+
experimental_telemetry: { isEnabled: true, functionId: 'aristotle-assistant', metadata: { userId: user.id, tenantId, contextPage: contextPage || '' } },
237238
onFinish: async (event) => {
238239
if (event.text) {
239240
await supabase.from('aristotle_messages').insert({

app/api/chat/exercises/student/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export async function POST(req: Request) {
4444
system: PROMPTS.exerciseCoach(exercise),
4545
messages: await convertToModelMessages(messages),
4646
tools: createAITools(supabase, { exerciseId, userId: user.id, tenantId, exerciseType: (exercise as any).exercise_type }),
47+
experimental_telemetry: { isEnabled: true, functionId: 'exercise-coach', metadata: { exerciseId: String(exerciseId), userId: user.id, tenantId } },
4748
onFinish: async (event) => {
4849
await supabase.from('exercise_messages').insert({
4950
exercise_id: exerciseId,

app/api/chat/lesson-task/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export async function POST(req: Request) {
6060
system: PROMPTS.lessonTutor(lesson, aiTask),
6161
messages: await convertToModelMessages(messages),
6262
tools: createAITools(supabase, { lessonId, userId: user.id, courseId: lesson.course_id, tenantId }),
63+
experimental_telemetry: { isEnabled: true, functionId: 'lesson-tutor', metadata: { lessonId: String(lessonId), userId: user.id, tenantId } },
6364
onFinish: async (event) => {
6465
const messageData: any = {
6566
lesson_id: lessonId,

app/api/teacher/preview/exercise/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export async function POST(req: Request) {
1818
model: AI_MODELS.coach,
1919
system: PROMPTS.previewExercise(instructions, system_prompt),
2020
messages: await convertToModelMessages(messages),
21+
experimental_telemetry: { isEnabled: true, functionId: 'preview-exercise', metadata: { userId: user.id } },
2122
stopWhen: stepCountIs(AI_CONFIG.maxSteps),
2223
})
2324

app/api/teacher/preview/lesson-task/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export async function POST(req: Request) {
1818
model: AI_MODELS.tutor,
1919
system: PROMPTS.previewLesson(task_description, system_prompt),
2020
messages: await convertToModelMessages(messages),
21+
experimental_telemetry: { isEnabled: true, functionId: 'preview-lesson-task', metadata: { userId: user.id } },
2122
})
2223

2324
return result.toUIMessageStreamResponse()

app/api/teacher/templates/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ export async function GET(req: NextRequest) {
1414
const category = searchParams.get('category')
1515
const search = searchParams.get('search')
1616

17+
// System templates have no tenant_id; user templates are scoped to tenant
1718
let query = supabase
1819
.from('prompt_templates')
1920
.select('*')
20-
.eq('tenant_id', tenantId)
21+
.or(`tenant_id.eq.${tenantId},tenant_id.is.null`)
2122
.or(`created_by.eq.${user.id},is_system.eq.true`)
2223
.order('is_system', { ascending: false })
2324
.order('created_at', { ascending: false })
@@ -34,6 +35,7 @@ export async function GET(req: NextRequest) {
3435
// POST /api/teacher/templates
3536
export async function POST(req: NextRequest) {
3637
const supabase = await createClient()
38+
const tenantId = await getCurrentTenantId()
3739
const { data: { user } } = await supabase.auth.getUser()
3840

3941
if (!user) return new Response('Unauthorized', { status: 401 })
@@ -45,7 +47,8 @@ export async function POST(req: NextRequest) {
4547
.insert({
4648
...body,
4749
created_by: user.id,
48-
is_system: false
50+
is_system: false,
51+
tenant_id: tenantId,
4952
})
5053
.select()
5154
.single()

instrumentation.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ import * as Sentry from "@sentry/nextjs";
33
export async function register() {
44
if (process.env.NEXT_RUNTIME === "nodejs") {
55
await import("./sentry.server.config");
6+
7+
// Langfuse AI observability via OpenTelemetry
8+
const { LangfuseSpanProcessor } = await import("@langfuse/otel");
9+
const { NodeTracerProvider } = await import("@opentelemetry/sdk-trace-node");
10+
11+
const langfuseSpanProcessor = new LangfuseSpanProcessor({
12+
// Only export AI SDK spans, not Next.js infra spans
13+
shouldExportSpan: (span) => {
14+
return span.otelSpan.instrumentationScope.name !== "next.js";
15+
},
16+
});
17+
18+
const tracerProvider = new NodeTracerProvider({
19+
spanProcessors: [langfuseSpanProcessor],
20+
});
21+
22+
tracerProvider.register();
623
}
724

825
if (process.env.NEXT_RUNTIME === "edge") {

0 commit comments

Comments
 (0)