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

yext / search-ui-react / 20959975951

13 Jan 2026 02:15PM UTC coverage: 85.288% (-0.5%) from 85.752%
20959975951

Pull #508

github

web-flow
Merge 54489ca8f into 4ee1e8b2b
Pull Request #508: ksearch: upgrade to Events API

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.08 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