refactor(headless): inject answer generation analytics client to listener middleware - #8045
refactor(headless): inject answer generation analytics client to listener middleware#8045mmitiche wants to merge 2 commits into
Conversation
Replace the hardcoded search analytics imports in the answer/follow-up streaming strategies with an injected AnswerGenerationAnalyticsClient. The client is threaded to the global generation listener middleware via ThunkExtraArguments (search engine injects the search implementation, with a search fallback) and to the follow-ups controller via a constructor param. Search behavior is unchanged; insight can later supply its own implementation.
|
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
|
Tip All tests passed and all changes approved!🟢 UI Tests: 456 tests unchanged |
@coveo/atomic
@coveo/atomic-hosted-page
@coveo/atomic-legacy
@coveo/atomic-react
@coveo/auth
@coveo/bueno
@coveo/create-atomic
@coveo/create-atomic-component
@coveo/create-atomic-component-project
@coveo/create-atomic-result-component
@coveo/create-atomic-rollup-plugin
@coveo/create-ui
@coveo/headless
@coveo/headless-react
@coveo/relay
@coveo/shopify
commit: |
…lyticsClient Instead of passing a separate generation analytics client to buildGeneratedAnswerWithFollowUps, reuse the controller's existing analyticsClient by having GeneratedAnswerAnalyticsClient extend AnswerGenerationAnalyticsClient. The search and insight client objects already exposed logGeneratedAnswerStreamEnd; logGeneratedAnswerResponseLinked is added to both. Drops the extra controller parameter and the standalone searchAnswerGenerationAnalyticsClient constant; the engine now injects generatedAnswerAnalyticsClient.
The problem
Most Headless controllers ship in two flavors for two use cases: search and insight. Take the pager: both versions consume the same core pager controller, and the only real difference is the analytics client they use — one dispatches search analytics actions, the other insight analytics actions. Each version is then exported under the same name but from a different bundle, so importing from
@coveo/headlessgives you the search controller and importing from the insight bundle gives you the insight one. Clean, and it works well across basically every controller.The
GeneratedAnswerWithFollowUpscontroller is the odd one out. This controller is not the thing that generates the answer. The answer is generated by a listener middleware that reacts to search queries and does the streaming work. So even though the controller could own a use-case-specific analytics client, it isn't the one actually producing the answer — which makes it tricky to log the right analytics for the right use case. In practice the generation lifecycle events (logGeneratedAnswerStreamEnd,logGeneratedAnswerResponseLinked) were just hardcoded to the search analytics actions inside the streaming strategies, so there was no way for insight to ever log them correctly.The fix
Instead of hardcoding, we introduce an
AnswerGenerationAnalyticsClientand inject it. The key insight is where it gets injected: as an extra argument at engine build time (ThunkExtraArguments).That's what makes this scale to both use cases in the future. The generation middleware is registered once for every engine, so it can't know on its own whether it's serving search or insight. But each engine builder already knows exactly which use case it is:
buildSearchEngineinjects the search generation analytics client.buildInsightEnginecan inject the insight one.The middleware just reads whatever client the engine put in
ThunkExtraArgumentsand hands it to the streaming strategy. So the same global generation machinery ends up logging search analytics inside a search engine and insight analytics inside an insight engine — no branching, no use-case detection, just the client the engine was built with. The follow-ups controller gets the same client through its constructor (forretry()and follow-up streaming), fed from the same per-use-case constant.Scope
CustomAction | InsightAction) so dropping in an insight client later is a one-liner in the insight engine + bundle.