docs: add web callback docs#2973
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@claude review |
There was a problem hiding this comment.
LGTM — docs-only addition for a new feature; the inline nits are cosmetic and the github-stars bump is routine.
Extended reasoning...
Overview
This PR adds a new docs page at content/docs/observability/features/web-callbacks.mdx documenting the Web Callbacks feature, registers it in the sibling meta.json sidebar config, and bumps the cached GITHUB_STARS constant. No production application code is touched.
Security risks
None substantive. The doc itself contains correct security guidance (warns against putting secrets in browser-visible headers, recommends keeping API credentials server-side, recommends validating bodies in production). The example receiver listens on 127.0.0.1 and is presented as a minimal illustration.
Level of scrutiny
Low. This is a docs-only PR plus a mechanical star-count refresh; no logic, auth, or rendering code is affected. The risk surface is limited to user-facing copy.
Other factors
Two nits were flagged inline (casing inconsistency between title/sidebarTitle/H1, and prose claiming the minimal receiver "validates" the payload when it only does a TS type assertion). Both are cosmetic and don't block approval — the author can address them in a follow-up if desired.
| - Return any HTTP `2xx` status for success. | ||
| - Respond before the configured timeout. | ||
| - Allow browser requests from your Langfuse origin via CORS. | ||
| - Respond to CORS preflight `OPTIONS` requests if you configure custom headers. |
There was a problem hiding this comment.
🔴 The prose at lines 69 and 155 says CORS preflight is needed "if you configure custom headers", but a POST with Content-Type: application/json (which Langfuse sends, per line 36) always triggers a CORS preflight regardless of any custom headers — application/json is not a CORS-safelisted Content-Type per the Fetch spec. The example code on lines 112–116 correctly handles OPTIONS unconditionally, but a reader following the prose literally could omit OPTIONS handling assuming they have no custom headers and then hit a preflight failure on the first callback. Suggested fix: reword both lines to attribute preflight to Content-Type: application/json rather than custom headers.
Extended reasoning...
What the bug is. Lines 69 and 155 of content/docs/observability/features/web-callbacks.mdx both condition CORS preflight handling on "custom headers":\n\n- Line 69 (Endpoint requirements): "Respond to CORS preflight OPTIONS requests if you configure custom headers."\n- Line 155 (Troubleshooting → CORS error): "If you use custom headers, the browser may send an OPTIONS preflight request before the POST; your server must handle that request too."\n\nBoth statements are incorrect. Per the Fetch spec, only three Content-Type values qualify a request as a CORS "simple request": application/x-www-form-urlencoded, multipart/form-data, and text/plain. application/json is not safelisted, so any POST with that Content-Type triggers a preflight unconditionally — custom headers or not.\n\nWhy this contradicts the doc itself. Three independent signals in the same file confirm preflight is always required:\n\n1. Line 36 explicitly says "Langfuse sends a JSON POST request", i.e. Content-Type: application/json.\n2. Line 110 of the example sets Access-Control-Allow-Headers: "content-type" — that header is only meaningful when Content-Type itself is a non-safelisted value and therefore subject to preflight checks.\n3. Lines 112–116 of the example handle OPTIONS unconditionally, with no "if custom headers" branch. The example correctly reflects that preflight is always needed; only the prose is wrong.\n\nImpact. This is a docs-accuracy bug, not a runtime bug — but it actively misleads implementers on their very first integration. A reader who configures no custom headers will follow line 69 literally, skip OPTIONS handling in their receiver, and the browser will reject the request before any POST body is sent. The error will surface as a CORS preflight failure, which is exactly the symptom described in the troubleshooting section the prose misexplains.\n\nStep-by-step proof. Suppose a user reads the doc and writes a minimal receiver that handles only POST (because they configured no custom headers):\n\n1. User triggers a web callback from a Langfuse trace.\n2. Browser sees outbound request: POST <endpoint> with Content-Type: application/json.\n3. Per Fetch spec §4.3, this is not a simple request (application/json is not in the safelisted Content-Type set), so the browser issues a CORS preflight: OPTIONS <endpoint> with Access-Control-Request-Method: POST and Access-Control-Request-Headers: content-type.\n4. The user's receiver returns 405 Method Not Allowed (or whatever its non-POST default is) — the preflight fails.\n5. The browser never sends the actual POST. Langfuse shows the "CORS error" toast.\n6. The user re-reads line 69, sees "if you configure custom headers", and is confused because they didn't configure any.\n\nHow to fix. Reword both lines to attribute preflight to the Content-Type, not custom headers. For example:\n\n- Line 69: "Respond to CORS preflight OPTIONS requests, because the callback uses Content-Type: application/json."\n- Line 155: "Because the request uses Content-Type: application/json, the browser sends an OPTIONS preflight before the POST; your server must handle that request too."
| const server = http.createServer((req, res) => { | ||
| res.setHeader("Access-Control-Allow-Origin", "https://cloud.langfuse.com"); | ||
| res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); | ||
| res.setHeader("Access-Control-Allow-Headers", "content-type"); |
There was a problem hiding this comment.
🟡 The example sets Access-Control-Allow-Headers: "content-type" but step 4 above invites users to configure "optional browser-visible headers" on the endpoint. Any custom header configured in the Langfuse UI (e.g. X-Trace-Source) will appear in the preflight's Access-Control-Request-Headers and be rejected by this static allowlist — causing a confusing CORS failure. Consider adding a comment noting that any custom headers configured in Langfuse must be appended to Access-Control-Allow-Headers, or calling this out in the troubleshooting CORS section (which currently mentions preflight but not the Allow-Headers requirement).
Extended reasoning...
What the bug is. In content/docs/observability/features/web-callbacks.mdx, the "Configure a web callback" section (step 4, line 23) tells the reader they can configure "optional browser-visible headers" on the endpoint. The "Browser-side delivery" section (line 81) reiterates that custom headers are visible in browser dev tools. But the minimal receiver example (line 110) hardcodes:
res.setHeader("Access-Control-Allow-Headers", "content-type");A reader who follows step 4 and adds any custom header on the Langfuse side will hit a CORS preflight failure the moment they wire up this example.
The code path that triggers it. Per the CORS spec, when the browser issues a non-simple request (here a JSON POST with a custom header), it first sends an OPTIONS preflight whose Access-Control-Request-Headers lists every non-CORS-safelisted header the actual request will send. The server's Access-Control-Allow-Headers response must be a superset of that list, or the browser rejects the preflight and never issues the POST.
Why existing code/docs don't prevent it. The "Endpoint requirements" section (line 69) acknowledges that custom headers trigger preflight. The "Troubleshooting > CORS error" section (line 155) repeats that the server must handle preflight — but neither section actually names Access-Control-Allow-Headers or tells the reader to extend it. The "For production" guidance (line 145) calls out tightening Access-Control-Allow-Origin, validating the body, and keeping credentials on the server, but never mentions Allow-Headers. So a user who diligently reads the whole page can still miss this.
Impact. Pure documentation completeness. The example works fine for the default no-custom-headers case (which is why this is a nit, not a normal-severity bug). But the doc itself invites the very configuration that breaks the example, and the failure mode — a CORS preflight error toast on first click — is exactly the kind of friction users blame the example for.
Step-by-step proof.
- User follows step 4 and adds a custom header
X-Trace-Source: uion the Langfuse endpoint config. - User copies the minimal receiver example verbatim and runs it on
http://127.0.0.1:4047. - User clicks "Call ..." on a trace. The browser prepares a
POSTwith headerscontent-type: application/jsonandx-trace-source: ui. - Because
x-trace-sourceis not on the CORS-safelisted-request-header list, the browser first sendsOPTIONS /withAccess-Control-Request-Headers: content-type, x-trace-source. - The example server responds
204withAccess-Control-Allow-Headers: content-type. - The browser sees
x-trace-sourceis missing from the allowlist and aborts the preflight — thePOSTis never sent. - Langfuse surfaces a CORS-error toast; nothing in the troubleshooting section points the reader back to the example's hardcoded Allow-Headers value.
How to fix. The lowest-friction fix is a one-line code comment in the example, e.g. // Append any custom headers configured in Langfuse (e.g. "content-type, x-trace-source"). Alternatively, extend the "CORS error" troubleshooting section to explicitly name Access-Control-Allow-Headers and tell readers to list every custom header configured in the Langfuse UI.
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Clemo <121163007+clemra@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tobias Wochinger <tobias.wochinger@clickhouse.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Lotte Verheyden <48100308+Lotte-Verheyden@users.noreply.github.com> Co-authored-by: Jannik Maierhöfer <48529566+jannikmaierhoefer@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jannik Maierhöfer <jannikmaierhoefer@users.noreply.github.com>
…3009) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jannik Maierhöfer <48529566+jannikmaierhoefer@users.noreply.github.com>
Co-authored-by: Lotte Verheyden <48100308+Lotte-Verheyden@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jannik Maierhöfer <48529566+jannikmaierhoefer@users.noreply.github.com>
Co-authored-by: Jannik Maierhöfer <48529566+jannikmaierhoefer@users.noreply.github.com> Co-authored-by: Jannik Maierhöfer <jannik@langfuse.com>
…thub-actions group (#3024) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jannik Maierhöfer <48529566+jannikmaierhoefer@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…3008) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
| className="flex flex-col gap-1 px-5 py-[22px]" | ||
| style={{ | ||
| borderRight: i < 3 ? "1px solid var(--line-structure)" : "none", | ||
| borderTop: "1px solid var(--line-structure)", | ||
| }} | ||
| > | ||
| <span className="text-[13px] font-medium text-text-primary leading-[1.4]"> | ||
| {top} |
There was a problem hiding this comment.
🟡 The Hero stat bar grid is grid-cols-2 md:grid-cols-4 (lines 174–199), but the inline borderRight: i < 3 rule is hard-coded for the 4-column desktop layout. On mobile, cell at index 1 is the rightmost of row 0 yet still gets a right border, producing an extra 1px line flush against the container's own right border. Fix by either dropping the right border on sm:nth-child(2n) via Tailwind responsive classes, or computing the boundary from the actual column count.
Extended reasoning...
What the bug is. In components/launch-week-5/LaunchWeek5Landing.tsx, the Hero stat bar renders four tiles inside a grid grid-cols-2 md:grid-cols-4 container. Each tile applies an inline style borderRight: i < 3 ? '1px solid var(--line-structure)' : 'none'. That mapping is correct for the desktop 4-column layout (separators after cells 0/1/2, none after cell 3), but it is responsive-naive: on mobile the grid wraps to 2 columns, so cell at index 1 is the rightmost cell of row 0 and should not have a right border — yet 1 < 3 is true and it still gets one.\n\nThe code path that triggers it. cornerBoxBase includes border border-line-structure lw5-corners, so the container itself has a real 1px Tailwind border on all sides (the lw5-corners overlay just adds bracket masks on top). At the grid-cols-2 breakpoint, cell 1's inline borderRight: 1px solid var(--line-structure) lands immediately adjacent to that container right border, producing either a doubled 2px-thick edge on row 0 or — depending on subpixel rendering and the bracket mask — a visible stray vertical line. Row 1 (cells 2 and 3) is fine: cell 2's right border merges into cell 3's left edge, and cell 3 already returns 'none'.\n\nWhy existing code doesn't prevent it. The author explicitly handled the right-edge case at i === 3 but only for the 4-column shape. There is no breakpoint-aware check, so the mobile 2-column shape inherits the wrong assumption. borderTop is applied unconditionally and looks correct at both breakpoints; only the right column on mobile is affected. The outer container's no-tl no-tr (and the preceding box's no-bl no-br) keeps the top/bottom corner brackets aligned, so this isolates the bug to the right border on row 0 at narrow viewports.\n\nStep-by-step proof.\n1. Open /launch-week-5 on a viewport < 768px (or resize a desktop browser to the sm breakpoint).\n2. The stat bar renders 2×2: cells [0,1] on row 0 and [2,3] on row 1.\n3. Inspect cell at index 1 ('Live demos' / 'ClickHouse OpenHouse'): it has border-right: 1px solid var(--line-structure) because 1 < 3.\n4. The container has its own 1px right border via cornerBoxBase ('relative bg-surface-bg border border-line-structure lw5-corners').\n5. Result: row 0's right edge renders as a doubled/thicker line than row 1's right edge (where cell 3 correctly returns 'none'), or as a stray 1px artifact depending on the bracket mask alignment.\n6. At md+ the grid becomes grid-cols-4, indices 0/1/2 all have a neighbor to the right that consumes their right border, and i === 3 correctly returns 'none' — so desktop is unaffected.\n\nImpact. Purely cosmetic, only visible at viewports below md (< 768px). No functional regression, no broken layout, no accessibility impact. Both borders use the same --line-structure color so it reads as a thickness mismatch rather than a colored stray line — still a polish issue on a public-facing launch page.\n\nHow to fix. Two clean options:\n- Tailwind-only: replace the inline style with responsive border classes — e.g. border-r border-line-structure md:border-r last:border-r-0 sm:nth-of-type(2n):border-r-0 (or hand-roll [&:nth-child(2n)]:sm:border-r-0 and md:[&:nth-child(2n)]:border-r plus md:last:border-r-0). This keeps the boundary purely declarative.\n- Compute from column count: track the active column count (4 on md+, 2 below) and derive the boundary, e.g. borderRight: ((i + 1) % cols === 0) ? 'none' : '1px solid var(--line-structure)'. Less idiomatic in Tailwind-land but works without ditching the inline-style approach the file already uses.
| <Callout type="info"> | ||
| There are two types of automated evaluators to attach scores to traces: | ||
| There are two types of automated evaluators to attach scores to traces: | ||
| - LLM-as-a-judge (for quality signals or behavioral patterns like user disagreement) | ||
| - Code-based evaluators (for precise checks like whether the response contains a certain word or exceeds a length limit). | ||
| - [Code evaluators](/docs/evaluation/evaluation-methods/code-evaluators) (for precise checks on observations or experiments, not traces, like whether the response contains a certain word or exceeds a length limit). | ||
|
|
||
| More on both can be found in the [Evaluate](/academy/evaluate) section. | ||
|
|
There was a problem hiding this comment.
🟡 The Callout intro at line 69 ("There are two types of automated evaluators to attach scores to traces:") directly contradicts the new code-evaluator bullet at line 71, which now says code evaluators run "on observations or experiments, not traces". The PR added the "not traces" qualifier on the bullet without updating the intro, so the same Callout simultaneously promises and denies that scores attach to traces. Easiest fix: reword the intro to drop "to traces" (e.g. "There are two types of automated evaluators:" or "...to add scores to your data").
Extended reasoning...
Self-contradictory Callout in the Academy monitoring doc
The diff for content/academy/monitoring/index.mdx changes the second bullet of the Callout from:
Code-based evaluators (for precise checks like whether the response contains a certain word or exceeds a length limit).
to:
Code evaluators (for precise checks on observations or experiments, not traces, like whether the response contains a certain word or exceeds a length limit).
But the intro line right above it (line 69) was left untouched:
There are two types of automated evaluators to attach scores to traces:
So the Callout now reads as: "two types of automated evaluators to attach scores to traces" → "code evaluators … on observations or experiments, not traces". For the same item it simultaneously claims scores go to traces and that they explicitly do not go to traces.
Why this matters
This is an Academy page whose entire purpose is to teach readers Langfuse's evaluation model. Readers landing here are forming their mental model of where scores live (traces vs. observations vs. experiments). A contradictory paragraph in introductory material is exactly the kind of confusion that reverberates into follow-up support questions, especially since the diff also introduces a brand new "Code evaluators" page that draws a hard line between observation/experiment-level and trace-level evaluation. The intro line undercuts that distinction at the very moment the doc tries to establish it.
Step-by-step proof
- Open
content/academy/monitoring/index.mdxat line 68. - Line 69 (unchanged by the PR):
There are two types of automated evaluators to attach scores to traces: - Line 70 (LLM-as-a-judge bullet): consistent with the intro — judges do score traces.
- Line 71 (changed by the PR):
[Code evaluators](/docs/evaluation/evaluation-methods/code-evaluators) (for precise checks on observations or experiments, not traces, like whether the response contains a certain word or exceeds a length limit). - Within four lines the same Callout says scores are attached to traces (line 69) and that code evaluator scores are explicitly not attached to traces (line 71), for one of the two types it just enumerated.
- The git diff in the PR shows the change is scoped to the bullet only — the intro line is not touched in this PR's hunk for this file, so the contradiction is PR-introduced (the prior bullet was generically "Code-based evaluators (for precise checks like …)" with no targeting qualifier, which was consistent with the intro).
Impact
Cosmetic/docs-clarity, not a runtime bug — but the file is teaching the evaluation model in the Academy section, so reader confusion has outsized leverage here. It also conflicts with the framing the rest of this PR is trying to land (content/docs/evaluation/evaluation-methods/code-evaluators.mdx is careful to say code evaluators target observations or experiments).
How to fix
One-line edit to the intro on line 69. Any of these resolves it:
There are two types of automated evaluators:There are two types of automated evaluators to add scores to your data:There are two types of automated evaluators to attach scores to traces, observations, or experiments:
The third option is the most informative but also the longest; the first is the safest minimal fix.
No description provided.