Fix mount-time settings clobbering in website chat - #19828
Conversation
The ChatProvider effects each spread a stale settings object, so on mount only the last write survived and page-context prompts, example questions, and a restored chatId were dropped. Use functional setState updates so each write merges into the latest state. Keep the Inkeep onEvent callback fresh instead of freezing it at mount, and attach one delegated click listener on the chat shadow root with cleanup on unmount instead of one listener per link per answer. Add two capture calls so the surface is no longer blind. Generated-By: PostHog Desktop Task-Id: f88da3bf-5254-4f0f-a980-37c3716f105b
🦔 PostHog Review reviewed this pull requestFound 0 must fix, 4 should fix, 0 consider. Published 4 findings (view the review). Resolved comments: 3 fixed |
Deploy preview
|
Bundle reportTotal JS (gzip)8.15 MiB (+0.1 KiB / +0.0%) Eager graph (modules shipped in each entrypoint's initial chunks)
Largest modules in the
|
| Module | Size |
|---|---|
./src/data/mcp-tools.json |
1054.5 KiB |
css ./node_modules/.pnpm/css-loader@5.2.7_webpack@5.101.3/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[1]!./node_modules/.pnpm/postcss-loader@4.3.0_postcss@8.5.6_webpack@5.101.3/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[2]!./src/styles/global.css |
754.5 KiB |
./src/components/Stickers/Stickers.tsx |
696.4 KiB |
./node_modules/.pnpm/@radix-ui+react-icons@1.3.2_react@18.3.1/node_modules/@radix-ui/react-icons/dist/react-icons.esm.js |
481.4 KiB |
./node_modules/.pnpm/rehype-raw@7.0.0/node_modules/rehype-raw/lib/index.js + 29 modules |
395.1 KiB |
./src/hooks/useCustomers.tsx + 55 modules |
370.0 KiB |
./node_modules/.pnpm/@posthog+icons@0.36.6_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
354.8 KiB |
./node_modules/.pnpm/react-markdown@8.0.7_@types+react@16.14.66_react@18.3.1/node_modules/react-markdown/lib/react-markdown.js + 88 modules |
351.4 KiB |
./src/components/ProductComparisonTable/index.tsx + 126 modules |
301.7 KiB |
./node_modules/.pnpm/cloudinary-core@2.14.0_lodash@4.17.21/node_modules/cloudinary-core/cloudinary-core.js |
281.9 KiB |
./src/components/SearchUI/index.tsx + 87 modules |
273.0 KiB |
./node_modules/.pnpm/@posthog+brand@0.8.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/magnifying-glass.mjs |
254.7 KiB |
./node_modules/.pnpm/framer-motion@10.18.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/framer-motion/dist/es/render/dom/motion.mjs + 109 modules |
253.9 KiB |
./node_modules/.pnpm/d3@7.9.0/node_modules/d3/src/index.js + 208 modules |
247.4 KiB |
./src/components/Pricing/PricingSlider/Slider.tsx + 87 modules |
240.1 KiB |
Eager-graph budgets are report-only until a baseline is established. Sizes are gzip of public/**/*.js; eager size is webpack module source bytes for the modules actually shipped in the entrypoint's initial chunks (post-tree-shake).
|
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
There was a problem hiding this comment.
PostHog Review
Found 4 should fix.
Other findings (outside the changed lines)
Valid issues on this PR's files that sit on lines GitHub won't let us comment on inline.
Question analytics omit all follow-up questions
Priority: should_fix | File: src/hooks/useChat.tsx:82-86 | Category: bug
Why we think it's a valid issue
- Checked: the
logEventCallbackbody atsrc/hooks/useChat.tsx:80-124, thefirstResponsestate and where it resets, the effect that reinstallsonEventatsrc/hooks/useChat.tsx:179-186, the provider key atsrc/hooks/useChat.tsx:262, and every otherposthog.capturecall site insrc/. - Found:
src/hooks/useChat.tsx:82guards the whole block with!firstResponse, and the capture at line 86 sits inside that block. Line 83 setsfirstResponsefrom the first user message, so the guard is false for the rest of the conversation. The capture atsrc/hooks/useChat.tsx:93-94carries no equivalent guard, and Inkeep raisesassistant_answer_displayedonce per answer. - Found: this PR is what makes the guard effective. Before the change,
onEventheld a mount-time closure, sofirstResponsestayednulland line 86 would have fired on each message. The new effect atsrc/hooks/useChat.tsx:179-186reinstallsonEventwhenlogEventCallbackchanges, so the callback now reads the currentfirstResponse. The one-per-conversation behavior arrives with this change. - Found: no other call site captures a chat question. The only other chat captures are
Opened MaxAI chatatsrc/components/AskAIInput/index.tsx:29andsrc/components/AskMax/index.tsx:47, which record a chat open, not a question. - Found:
src/hooks/useChat.tsx:262keysChatProvideron the chat id or path, sofirstResponseresets only when a new conversation mounts. The event therefore counts conversations, while its name stateschat question submitted. - Impact: every follow-up question in a conversation produces a
chat answer displayedevent with no matchingchat question submittedevent. The two counts diverge by the number of follow-ups, so a question-to-answer comparison is wrong and total question volume reads low. Multi-turn use stays invisible in the data. The PR adds these events to size the chat in data, so the wrong count works against the reason for the change. The fix moves one line out of the guard and keepssetFirstResponseinside it.
Issue description
The capture call is inside the !firstResponse condition. It records only the first user message. The answer event records every answer. Follow-up questions therefore create unmatched answer events and incorrect conversion counts.
Suggested fix
Capture every user_message_submitted event. Keep only setFirstResponse(...) inside the if (!firstResponse) condition.
… root The link-interception listener was attached to the Inkeep shadow root and guarded by a ref so it installs only once. React replaces that shadow host whenever the viewport crosses the 768px mobile breakpoint, because `Container` swaps between `ScrollArea` and `React.Fragment`. After a wide -> narrow -> wide crossing the ref still points at the detached root, so the guard blocks re-attachment and internal doc links in later answers do a full page load instead of opening a desktop-OS window. In narrow mode the old selector matched the `ScrollArea` div, which has no shadow root, so interception never installed at all. Attach one delegated click listener to the stable `#embedded-chat-target` container and use `composedPath()` to find the anchor across the shadow boundary. The container is never replaced on resize, so the once-only guard is now correct and links keep custom navigation in both viewport modes. Generated-By: PostHog Desktop Task-Id: 7203edaa-ab3e-4e97-9a31-69c7949a481c
`InkeepSharedSettings` declared the two setters as
`(settings: InkeepBaseSettings) => void` / `(settings: InkeepAIChatSettings) => void`,
which accept only a complete object. The effects that now merge with a functional
updater — `setBaseSettings((prev) => ({ ...prev, ... }))` and the three
`setAiChatSettings` calls — therefore fail strict type checking (TS2560, plus an
implicit-any `prev` under TS7006). The narrow type is also what pushes writers
toward whole-object spreads, the very fault the surrounding fixes repair.
Type both setters as `Dispatch<SetStateAction<...>>`, which is exactly what the
underlying `useState` returns, so the functional updates type-check and each
`prev` is typed. Type-only change; no runtime effect.
Generated-By: PostHog Desktop
Task-Id: 7203edaa-ab3e-4e97-9a31-69c7949a481c
Inkeep can resolve a streamed answer after `ChatProvider` unmounts (submit a question, then close the chat before the answer arrives). The captured `baseSettings.onEvent` still runs `logEventCallback` on the dead provider, which then fires a phantom `chat answer displayed` capture for an answer no user saw — corrupting the metric this PR adds — and, because the listener install queries the global `#embedded-chat-target`, can attach a stray click listener onto whichever chat is open next. With two capture listeners on one node, `stopPropagation` does not stop the sibling, so a single link click can navigate twice. Add an `isMountedRef`, set false in the unmount cleanup, and gate the `assistant_answer_displayed` branch on it, so a closed chat no longer records analytics or touches the next chat's DOM. `logConversation` still runs so a submitted-then-closed conversation is preserved in history. Generated-By: PostHog Desktop Task-Id: 7203edaa-ab3e-4e97-9a31-69c7949a481c
Changes
Fixes four defects in the posthog.com chat that a customer reported as "pretty much unusable." All four are in
src/hooks/useChat.tsx/src/hooks/useInkeepSettings.ts.Why: every visitor and prospect can open this chat. The defects sit on the main path of the feature, and cost the user correct answers and the ability to restore a past conversation.
setAiChatSettings({ ...aiChatSettings, ... })withaiChatSettingsmissing from their dependency arrays. On mount all three read the same initial object, so only the last write survived andpromptsandexampleQuestionswere dropped — the chat answered without knowing what page you were on. Each write is now a functional update, so nothing clobbers anything.chatIdand theme. The same stale-spread crossed hooks:useInkeepSettingswrotecolorMode, thenuseChatoverwrote it, and a later change could wipechatId, so restoring a conversation from history did not load it. Both writes are now functional merges.onEventclosure. The InkeeponEventcallback was installed once in a[]effect, so it held mount-timefirstResponse/hasFirstResponseforever. It is now reinstalled when the callback identity changes.assistant_answer_displayedadded click listeners to every link without removing them. One delegated listener now handles all links, and it is removed on unmount.Also adds two
posthog.capturecalls (chat question submitted,chat answer displayed) — the surface had no instrumentation, so the impact could not be sized in data.No visual change: the fixes restore correct behavior on an unchanged UI. The chat renders through a live external (Inkeep) service that needs an API key, so this was verified by code reasoning and static checks (prettier, type check), not a live reproduction — consistent with the report, which notes the surface is uninstrumented and could not be sized in data.
Checklist
vercel.jsonCreated with PostHog Desktop from this inbox report.