Fixed
Project:
Langfuse
Version:
1.0.0-alpha1
Component:
Code
Priority:
Normal
Category:
Feature request
Assigned:
Unassigned
Reporter:
Created:
24 Sep 2025 at 14:27 UTC
Updated:
10 Aug 2026 at 10:14 UTC
Jump to comment: Most recent
It would be nice if we could also use the feedback system that's available in Langfuse
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
abhisekmazumdarWhat's needed to make this work
ai_answersneeds this: score a Langfuse trace from feedback submitted after the fact. Wiring it up against a real, self-hosted Langfuse v4 instance surfaced two blockers, both reproduced live, not guessed. Here's what needs to land:1. Add
createScore()toLangFuseClientThe natural call a consumer makes is:
No such method exists: not on
Drupal\langfuse\LangFuseClient, not onLangFuseClientInterface, not on the underlyingdropsolid/langfuse-php-sdk. Calling it throws:2. It has to work without a live local
TraceobjectThe obvious implementation,
$client->getTrace($traceId)->score(...), doesn't hold up in practice. Feedback normally arrives in a separate, later request — the user reads the answer, then clicks a rating. By then the trace is gone from local state:LangfuseSyncSubscriber::onKernelTerminate()unconditionally clearslangfuse_current_trace_idat the end of every request, and (outside CLI)LangFuseClient::syncTraces()also deletes the wholelangfuse_active_tracesState cache onceshouldSyncNow()allows it. I reproduced this directly: wiped the same State keys the sync subscriber wipes, then confirmedgetTrace($traceId)returnsNULLafterward. SocreateScore()can't be built ongetTrace()at all — it needs its own path.The fix, verified live
Client::sendEvent()doesn't need a live localTraceobject — ascore-createevent only needstraceIdas a string:I built this event by hand and called
$client->sendEvent(['batch' => [$event]])directly against a trace whose local State cache I had already wiped (simulating the real cross-request case). It landed correctly — confirmed in ClickHouse'sscorestable (trace_id,name,value,commentall correct) and in the Langfuse UI.So, concretely, this needs:
on
LangFuseClient/LangFuseClientInterface, building this event directly and callingsendEvent()— nogetTrace()/getCurrentTrace()involved. Once that lands,ai_answers's feedback flow (thumbs up/down on an answer) needs no changes on its side to start working — it already callscreateScore()with exactly this signature; it's just calling a method that doesn't exist yet.Related: self-hosted v4 compatibility (context, not part of this ask)
Getting a self-hosted Langfuse v4 instance to accept the SDK's ingestion events at all (a prerequisite for hitting the bug above) required setting
LANGFUSE_MIGRATION_V4_WRITE_MODE=legacyandLANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR=dual_write. The defaultevents_onlymode rejectstrace-createoutright ("Event type not accepted"), and Langfuse's own v4 source marks the legacy routes as a deprecated migration-window shim, not the target architecture. The real v4-native path is OTLP via/api/public/otel/v1/traces, whichdropsolid/langfuse-php-sdkhas no support for at all — no OTel dependency anywhere in itscomposer.json. Not asking for an OTel rewrite here, just flagging how fragile the current self-hosted-v4 story is for anyone else hitting this.Related: #3561460: Force deepchat question/reply to be 1 trace
Same root cause as the deepchat multi-trace problem in #3561460: Force deepchat question/reply to be 1 trace — the module assumes one Drupal request equals one trace lifecycle. This surfaces the same assumption through a different door (feedback-after-the-fact instead of multi-ajax). The
createScore()fix above is deliberately scoped to avoid needing to solve that bigger problem: treating a score as a stateless event keyed by trace ID sidesteps the lifecycle question entirely for this case.Next I will try to submit an MR with the
createScore()method plus a test.Comment #4
abhisekmazumdarWhat this does
Adds
createScore()toLangFuseClient/LangFuseClientInterface, plus an optionallangfuse_feedbacksubmodule with ready-made thumbs-up/down buttons.Why
There was no way to submit a feedback score for a trace.
getTrace($traceId)->score(...)doesn't work in practice, since feedback usually arrives in a later request after the trace's local state is already gone.createScore()avoids this: it sends a score-create event keyed only by trace ID, no live local trace needed. Verified against a real Langfuse instance.Credit to Niels for the groundwork on the submodule: the JS, template, routing, and controller started from his commit on this issue's fork. Adapted it to use
createScore(), added a block plugin, and fixed the CSRF handling.How to test
langfuse.settingsat a real Langfuse instance. ddev-langfuse gives you one locally.createScore($traceId, 'test', 1.0)and confirm the score shows up in Langfuse.langfuse_feedbackand try the "Langfuse Feedback" block or its/langfuse/feedbackroute directly.Comment #5
nikro commentedComment #6
nikro commentedWent through the langfuse_feedback submodule properly and pushed a follow-up commit - with practical testing against an instance.
Pulled the Block plugin entirely. It can only ever render when something explicitly maps a
trace_idcontext (Layout Builder, or hand-written code) - plain block placement can never do that, and its own fallback (the request's "current trace") doesn't survive the common case where the trace was created in an earlier, separate request. Tried placing it myself and got nothing, for exactly this reason. The theme hook underneath it already renders standalone with no block involved, so nothing is lost by dropping it.Normalized the hardcoded score name from
user-feedbacktouser_feedback, added value validation (it took anything before, now it's numeric and 0-1), and switchedcatch (\Exception)tocatch (\Throwable)in the controller so a\TypeErrordoesn't slip past as an uncaught fatal instead of the 502 it's supposed to return.Bigger one: the endpoint had zero abuse protection — no rate limit, no ownership check on the trace id, reachable anonymously. Added flood control (core's flood service, no new dependency) at 20 submissions/hour. Then for repeat votes from the same person on the same trace, gave
createScore()an optional$idparam and derive it deterministically from identity + trace + score name. Confirmed live: sending the same id twice upserts the score rather than duplicating it, so changing your vote updates the existing score instead of piling up a new one each time.Also wrapped the button labels for translation, and added a real test file for the controller — flood limiting, validation, idempotent score ids, the Throwable fix — since there weren't any tests for this part before.
Everything's on the branch as a follow-up commit on top of the existing one. Let me know if you'd rather I split the block removal out separately.
Comment #7
nikro commentedAbhisek - can you run a small test and check the diff and see - if you agree with my changes, I'll merge tmw.
Comment #8
abhisekmazumdarComment #9
abhisekmazumdarI agree with all of it. Thank you.
Just AI highlighted a cspell error, which added a new commit.
Rest phpstan error are all existing.....
Comment #11
nikro commentedMerged, thank you! :)