Skip to content

Fix mount-time settings clobbering in website chat - #19828

Draft
posthog[bot] wants to merge 4 commits into
masterfrom
posthog-self-driving/fixchat-stop-mount-effects-clobbering-2b0b44
Draft

Fix mount-time settings clobbering in website chat#19828
posthog[bot] wants to merge 4 commits into
masterfrom
posthog-self-driving/fixchat-stop-mount-effects-clobbering-2b0b44

Conversation

@posthog

@posthog posthog Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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.

  • Lost page context and example questions. Three effects each did setAiChatSettings({ ...aiChatSettings, ... }) with aiChatSettings missing from their dependency arrays. On mount all three read the same initial object, so only the last write survived and prompts and exampleQuestions were dropped — the chat answered without knowing what page you were on. Each write is now a functional update, so nothing clobbers anything.
  • Dropped chatId and theme. The same stale-spread crossed hooks: useInkeepSettings wrote colorMode, then useChat overwrote it, and a later change could wipe chatId, so restoring a conversation from history did not load it. Both writes are now functional merges.
  • Frozen onEvent closure. The Inkeep onEvent callback was installed once in a [] effect, so it held mount-time firstResponse / hasFirstResponse forever. It is now reinstalled when the callback identity changes.
  • Leaked shadow-root listeners. Each assistant_answer_displayed added 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.capture calls (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

  • I've read the docs and/or content style guides.
  • Words are spelled using American English
  • Use relative URLs for internal links
  • I've checked the pages added or changed in the Vercel preview build
  • If I moved a page, I added a redirect in vercel.json

Created with PostHog Desktop from this inbox report.

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

posthog Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

🦔 PostHog Review reviewed this pull request

Found 0 must fix, 4 should fix, 0 consider.

Published 4 findings (view the review).

Resolved comments: 3 fixed

@github-actions github-actions Bot added the website About the website (beyond just landing pages) label Aug 30, 2026
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Deploy preview

Status Details Updated (UTC)
🟢 Ready View preview Aug 30, 2026 01:07PM

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Bundle report

Total JS (gzip)

8.15 MiB (+0.1 KiB / +0.0%)

Eager graph (modules shipped in each entrypoint's initial chunks)

Entrypoint Eager size Budget Modules
app 16.91 MiB (+2.5 KiB / +0.0%) report-only 2037
Largest modules in the app closure
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

posthog Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 logEventCallback body at src/hooks/useChat.tsx:80-124, the firstResponse state and where it resets, the effect that reinstalls onEvent at src/hooks/useChat.tsx:179-186, the provider key at src/hooks/useChat.tsx:262, and every other posthog.capture call site in src/.
  • Found: src/hooks/useChat.tsx:82 guards the whole block with !firstResponse, and the capture at line 86 sits inside that block. Line 83 sets firstResponse from the first user message, so the guard is false for the rest of the conversation. The capture at src/hooks/useChat.tsx:93-94 carries no equivalent guard, and Inkeep raises assistant_answer_displayed once per answer.
  • Found: this PR is what makes the guard effective. Before the change, onEvent held a mount-time closure, so firstResponse stayed null and line 86 would have fired on each message. The new effect at src/hooks/useChat.tsx:179-186 reinstalls onEvent when logEventCallback changes, so the callback now reads the current firstResponse. 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 chat at src/components/AskAIInput/index.tsx:29 and src/components/AskMax/index.tsx:47, which record a chat open, not a question.
  • Found: src/hooks/useChat.tsx:262 keys ChatProvider on the chat id or path, so firstResponse resets only when a new conversation mounts. The event therefore counts conversations, while its name states chat question submitted.
  • Impact: every follow-up question in a conversation produces a chat answer displayed event with no matching chat question submitted event. 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 keeps setFirstResponse inside 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.

Comment thread src/hooks/useChat.tsx Outdated
Comment thread src/hooks/useChat.tsx
Comment thread src/hooks/useChat.tsx Outdated
posthog Bot added 3 commits August 30, 2026 12:46
… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

website About the website (beyond just landing pages)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants