• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yext / search-ui-react / 20970077140

13 Jan 2026 07:38PM UTC coverage: 85.288% (-0.5%) from 85.752%
20970077140

push

github

web-flow
ksearch: upgrade to Events API (#508)

* ksearch: upgrade to Events API

This PR upgrades our analytics calls to the next major
version, also called the Events API. As part of this
work, analytics calls have changed shape, and some deprecated
properties have been dropped.
J=WAT-4651
TEST=auto,manual

Updated auto tests. Ran test site locally and saw events all the
way to snowflake.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>

1019 of 1401 branches covered (72.73%)

Branch coverage included in aggregate %.

104 of 127 new or added lines in 19 files covered. (81.89%)

2 existing lines in 2 files now uncovered.

2239 of 2419 relevant lines covered (92.56%)

141.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.02
/src/hooks/useCardAnalytics.ts
1
import {DirectAnswer as DirectAnswerData, DirectAnswerType, Result, useSearchState} from '@yext/search-headless-react';
20✔
2
import {useCallback} from 'react';
20✔
3
import {FeedbackType} from '../components/ThumbsFeedback';
4
import {DefaultRawDataType} from '../models/index';
5
import {useAnalytics} from './useAnalytics';
20✔
6
import {GdaClickEventData} from '../components/GenerativeDirectAnswer'
7
import {SearchAction} from "../models/SearchEventPayload";
8

9
/**
10
 * Analytics event types for cta click, title click, and citation click.
11
 *
12
 * @public
13
 */
14
export type CardCtaEventType = 'CTA_CLICK' | 'TITLE_CLICK' | 'CITATION_CLICK' | 'DRIVING_DIRECTIONS' | 'VIEW_WEBSITE' | 'TAP_TO_CALL';
15

16
/**
17
 * The data types use to construct the payload in the analytics event.
18
 *
19
 * @public
20
 */
21
export type CardAnalyticsDataType<T = DefaultRawDataType> = DirectAnswerData | Result<T> | GdaClickEventData;
22

23
/**
24
 * Analytics event types for interactions on a card.
25
 *
26
 * @public
27
 */
28
export type CardAnalyticsType = CardCtaEventType | FeedbackType;
29

30
function isDirectAnswer(data: unknown): data is DirectAnswerData {
31
  return (data as DirectAnswerData)?.type === DirectAnswerType.FeaturedSnippet ||
14!
32
      (data as DirectAnswerData)?.type === DirectAnswerType.FieldValue;
33
}
34

35
function isGenerativeDirectAnswer(data: unknown): data is GdaClickEventData {
36
  return !!(data as GdaClickEventData)?.destinationUrl;
2✔
37
}
38

39
export function useCardAnalytics<T>(): (
20✔
40
  cardResult: CardAnalyticsDataType<T>, analyticsEventType: CardAnalyticsType
41
) => void {
42
  const analytics = useAnalytics();
218✔
43
  const verticalKey = useSearchState(state => state.vertical?.verticalKey);
246✔
44
  const queryId = useSearchState(state => state.query.queryId);
246✔
45
  const searchId = useSearchState(state => state.meta.uuid);
246✔
46
  const locale = useSearchState(state => state.meta.locale);
246✔
47
  const experienceKey = useSearchState(state => state.meta.experienceKey);
246✔
48
  const searchTerm = useSearchState(state => state.query.mostRecentSearch);
246✔
49

50
  const reportCtaEvent = useCallback((
218✔
51
    result: CardAnalyticsDataType<T>,
52
    eventType: CardCtaEventType
53
  ) => {
54
    let url: string | undefined, entityId: string | undefined;
55
    let directAnswer = false;
4✔
56
    let generativeDirectAnswer = false;
4✔
57
    if (isDirectAnswer(result)) {
4✔
58
      url = result.relatedResult.link;
2✔
59
      entityId = result.relatedResult.id;
2✔
60
      directAnswer = true;
2✔
61
    } else if (isGenerativeDirectAnswer(result)) {
2!
62
      url = result.destinationUrl;
2✔
63
      entityId = result.searchResult?.id;
2✔
64
      directAnswer = true;
2✔
65
      generativeDirectAnswer = true;
2✔
66
    } else {
67
      url = result.link;
×
68
      entityId = result.id;
×
69
    }
70

71
    if (!queryId) {
4!
72
      console.error('Unable to report a CTA event. Missing field: queryId.');
×
73
      return;
×
74
    }
75
    if (!searchId) {
4!
NEW
76
      console.error('Unable to report a CTA event. Missing field: searchId.');
×
NEW
77
      return;
×
78
    }
79
    if (!experienceKey) {
4!
NEW
80
      console.error('Unable to report a CTA event. Missing field: experienceKey.');
×
NEW
81
      return;
×
82
    }
83
    // convert the legacy card event type to the format the current reporter expects.
84
    let action:SearchAction = eventType === 'TITLE_CLICK' ? 'TITLE' : eventType === 'VIEW_WEBSITE' ? 'WEBSITE': eventType;
4!
85
    analytics?.report({
4✔
86
      action: action,
87
      destinationUrl: url,
88
      entity: entityId,
89
      locale,
90
      searchId,
91
      queryId,
92
      verticalKey: verticalKey || '',
8✔
93
      isDirectAnswer: directAnswer,
94
      isGenerativeDirectAnswer: generativeDirectAnswer,
95
      experienceKey,
96
      searchTerm,
97
    });
98
  }, [analytics, queryId, verticalKey]);
99

100
  const reportFeedbackEvent = useCallback((
218✔
101
    result: CardAnalyticsDataType<T>,
102
    feedbackType: FeedbackType
103
  ) => {
104
    if (!queryId) {
10!
105
      console.error('Unable to report a result feedback event. Missing field: queryId.');
×
106
      return;
×
107
    }
108
    if (!searchId) {
10!
NEW
109
      console.error('Unable to report a result feedback event. Missing field: searchId.');
×
NEW
110
      return;
×
111
    }
112
    if (!experienceKey) {
10!
NEW
113
      console.error('Unable to report a result feedback event. Missing field: experienceKey.');
×
NEW
114
      return;
×
115
    }
116
    let directAnswer = false;
10✔
117
    let generativeDirectAnswer = false;
10✔
118
    let entityId: string | undefined;
119
    if (isDirectAnswer(result)) {
10!
120
      directAnswer = true;
10✔
121
      entityId = result.relatedResult.id;
10✔
122
    } else if (isGenerativeDirectAnswer(result)) {
×
123
      directAnswer = true;
×
NEW
124
      generativeDirectAnswer = true;
×
UNCOV
125
      entityId = result.searchResult?.id;
×
126
    } else {
127
      entityId = result.id;
×
128
    }
129
    analytics?.report({
10✔
130
      action: feedbackType,
131
      entity: entityId,
132
      locale,
133
      searchId,
134
      queryId,
135
      verticalKey: verticalKey || '',
20✔
136
      isDirectAnswer: directAnswer,
137
      isGenerativeDirectAnswer: generativeDirectAnswer,
138
      experienceKey,
139
      searchTerm
140
    });
141
  }, [analytics, queryId, verticalKey]);
142

143
  return useCallback((
218✔
144
      cardResult: CardAnalyticsDataType<T>,
145
      analyticsEventType: CardAnalyticsType
146
  ) => {
147
    if (!analytics) {
14!
148
      return;
×
149
    }
150
    if (analyticsEventType === 'TITLE_CLICK' || analyticsEventType === 'CTA_CLICK' || analyticsEventType === 'CITATION_CLICK' || analyticsEventType === 'DRIVING_DIRECTIONS' || analyticsEventType === 'VIEW_WEBSITE' || analyticsEventType === 'TAP_TO_CALL') {
14✔
151
      reportCtaEvent(cardResult, analyticsEventType);
4✔
152
    }
153
    if (analyticsEventType === 'THUMBS_DOWN' || analyticsEventType === 'THUMBS_UP') {
14✔
154
      reportFeedbackEvent(cardResult, analyticsEventType);
10✔
155
    }
156
  }, [analytics, reportCtaEvent, reportFeedbackEvent]);
157
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc