@redocly/theme 0.67.0-next.3 → 0.67.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/lib/components/Breadcrumbs/BreadcrumbDropdown.d.ts +2 -1
  2. package/lib/components/Breadcrumbs/BreadcrumbDropdown.js +9 -10
  3. package/lib/components/Breadcrumbs/Breadcrumbs.js +5 -22
  4. package/lib/components/Buttons/AIAssistantButton.js +2 -6
  5. package/lib/components/Buttons/EditPageButton.js +2 -1
  6. package/lib/components/Catalog/CatalogCardView/CatalogCard.js +1 -1
  7. package/lib/components/CatalogClassic/CatalogClassicActions.js +3 -1
  8. package/lib/components/CatalogClassic/CatalogClassicCard.js +1 -1
  9. package/lib/components/CatalogClassic/CatalogClassicInfoBlock.js +2 -1
  10. package/lib/components/Dropdown/Dropdown.d.ts +2 -0
  11. package/lib/components/Dropdown/Dropdown.js +8 -2
  12. package/lib/components/Feedback/ReportDialog.js +4 -1
  13. package/lib/components/Filter/FilterCheckboxes.js +3 -1
  14. package/lib/components/Footer/FooterItem.js +1 -1
  15. package/lib/components/LanguagePicker/LanguagePicker.js +3 -1
  16. package/lib/components/Logo/Logo.js +2 -1
  17. package/lib/components/Menu/MenuContainer.js +1 -0
  18. package/lib/components/Menu/MenuItem.js +21 -7
  19. package/lib/components/Navbar/Navbar.js +6 -2
  20. package/lib/components/Navbar/NavbarItem.js +3 -1
  21. package/lib/components/Search/SearchAiMessage.js +23 -5
  22. package/lib/components/Search/SearchDialog.js +5 -33
  23. package/lib/components/Search/SearchInput.js +4 -1
  24. package/lib/components/Search/SearchRecent.js +3 -1
  25. package/lib/components/SidebarActions/SidebarActions.js +10 -3
  26. package/lib/components/TableOfContent/TableOfContent.js +1 -1
  27. package/lib/components/UserMenu/LoginButton.js +2 -1
  28. package/lib/components/UserMenu/LogoutMenuItem.js +2 -1
  29. package/lib/core/hooks/search/use-search-dialog.js +7 -2
  30. package/lib/core/hooks/use-banner-telemetry.js +2 -11
  31. package/lib/core/hooks/use-color-switcher.js +2 -1
  32. package/lib/core/hooks/use-page-actions.js +12 -21
  33. package/lib/core/hooks/use-product-picker.js +14 -4
  34. package/lib/core/hooks/use-telemetry-fallback.d.ts +45 -37
  35. package/lib/core/hooks/use-telemetry-fallback.js +47 -39
  36. package/lib/core/types/search.d.ts +2 -1
  37. package/lib/core/types/search.js +1 -0
  38. package/lib/core/utils/index.d.ts +2 -0
  39. package/lib/core/utils/index.js +2 -0
  40. package/lib/core/utils/telemetry/generate-before-after-context.d.ts +37 -0
  41. package/lib/core/utils/telemetry/generate-before-after-context.js +32 -0
  42. package/lib/core/utils/telemetry/generate-resource-urn.d.ts +7 -0
  43. package/lib/core/utils/telemetry/generate-resource-urn.js +13 -0
  44. package/lib/core/utils/telemetry/get-base-data-attributes.d.ts +14 -0
  45. package/lib/core/utils/telemetry/get-base-data-attributes.js +17 -0
  46. package/package.json +3 -3
  47. package/src/components/Breadcrumbs/BreadcrumbDropdown.tsx +18 -4
  48. package/src/components/Breadcrumbs/Breadcrumbs.tsx +5 -6
  49. package/src/components/Buttons/AIAssistantButton.tsx +2 -3
  50. package/src/components/Buttons/EditPageButton.tsx +4 -1
  51. package/src/components/Catalog/CatalogCardView/CatalogCard.tsx +2 -2
  52. package/src/components/CatalogClassic/CatalogClassicActions.tsx +4 -2
  53. package/src/components/CatalogClassic/CatalogClassicCard.tsx +9 -2
  54. package/src/components/CatalogClassic/CatalogClassicInfoBlock.tsx +2 -1
  55. package/src/components/Dropdown/Dropdown.tsx +8 -1
  56. package/src/components/Feedback/ReportDialog.tsx +4 -1
  57. package/src/components/Filter/FilterCheckboxes.tsx +4 -2
  58. package/src/components/Footer/FooterItem.tsx +4 -2
  59. package/src/components/LanguagePicker/LanguagePicker.tsx +9 -2
  60. package/src/components/Logo/Logo.tsx +7 -1
  61. package/src/components/Menu/MenuContainer.tsx +1 -0
  62. package/src/components/Menu/MenuItem.tsx +35 -4
  63. package/src/components/Navbar/Navbar.tsx +7 -3
  64. package/src/components/Navbar/NavbarItem.tsx +7 -1
  65. package/src/components/Search/SearchAiMessage.tsx +25 -5
  66. package/src/components/Search/SearchDialog.tsx +6 -13
  67. package/src/components/Search/SearchInput.tsx +4 -1
  68. package/src/components/Search/SearchRecent.tsx +4 -2
  69. package/src/components/SidebarActions/SidebarActions.tsx +10 -3
  70. package/src/components/TableOfContent/TableOfContent.tsx +7 -2
  71. package/src/components/UserMenu/LoginButton.tsx +4 -1
  72. package/src/components/UserMenu/LogoutMenuItem.tsx +2 -1
  73. package/src/core/hooks/search/use-search-dialog.ts +13 -2
  74. package/src/core/hooks/use-banner-telemetry.ts +5 -4
  75. package/src/core/hooks/use-color-switcher.ts +7 -1
  76. package/src/core/hooks/use-page-actions.ts +17 -28
  77. package/src/core/hooks/use-product-picker.ts +19 -5
  78. package/src/core/hooks/use-telemetry-fallback.ts +47 -39
  79. package/src/core/types/search.ts +1 -0
  80. package/src/core/utils/index.ts +2 -0
  81. package/src/core/utils/telemetry/generate-before-after-context.ts +59 -0
  82. package/src/core/utils/telemetry/generate-resource-urn.ts +9 -0
  83. package/src/core/utils/telemetry/get-base-data-attributes.ts +27 -0
@@ -7,7 +7,7 @@ import type { SearchFacetCount, SearchItemData } from '@redocly/theme/core/types
7
7
  import { SearchInput } from '@redocly/theme/components/Search/SearchInput';
8
8
  import { SearchShortcut } from '@redocly/theme/components/Search/SearchShortcut';
9
9
  import { Button } from '@redocly/theme/components/Button/Button';
10
- import { breakpoints, concatClassNames } from '@redocly/theme/core/utils';
10
+ import { breakpoints, concatClassNames, getBaseDataAttributes } from '@redocly/theme/core/utils';
11
11
  import { Portal } from '@redocly/theme/components/Portal/Portal';
12
12
  import { SearchItem } from '@redocly/theme/components/Search/SearchItem';
13
13
  import { SearchRecent } from '@redocly/theme/components/Search/SearchRecent';
@@ -172,9 +172,8 @@ export function SearchDialog({
172
172
  addSearchHistoryItem(query);
173
173
  telemetry.sendSearchResultClickedMessage([
174
174
  {
175
- object: 'search',
175
+ ...getBaseDataAttributes('searchResultItem', 'search', item.document.url),
176
176
  query: query,
177
- url: item.document.url,
178
177
  totalResults: results.length.toString(),
179
178
  index: index.toString(),
180
179
  searchEngine: mode,
@@ -259,9 +258,7 @@ export function SearchDialog({
259
258
  }
260
259
  telemetry.sendSearchAiOpenedMessage([
261
260
  {
262
- id: 'searchAiButton',
263
- object: 'search',
264
- uri: 'urn:redocly:realm:ui:search:searchAiButton',
261
+ ...getBaseDataAttributes('searchAiButton', 'search'),
265
262
  method: 'ai_search_button',
266
263
  },
267
264
  ]);
@@ -355,9 +352,7 @@ export function SearchDialog({
355
352
  }
356
353
  telemetry.sendSearchAiOpenedMessage([
357
354
  {
358
- id: 'searchAiInput',
359
- object: 'search',
360
- uri: 'urn:redocly:realm:ui:search:searchAiInput',
355
+ ...getBaseDataAttributes('searchAiInput', 'search'),
361
356
  method: 'ai_search_input',
362
357
  },
363
358
  ]);
@@ -370,9 +365,7 @@ export function SearchDialog({
370
365
  }
371
366
  telemetry.sendSearchAiOpenedMessage([
372
367
  {
373
- id: 'searchAiInput',
374
- object: 'search',
375
- uri: 'urn:redocly:realm:ui:search:searchAiInput',
368
+ ...getBaseDataAttributes('searchAiInput', 'search'),
376
369
  method: 'ai_search_input',
377
370
  },
378
371
  ]);
@@ -474,7 +467,7 @@ export function SearchDialog({
474
467
  onSelect={(query, index) => {
475
468
  telemetry.sendSearchRecentClickedMessage([
476
469
  {
477
- object: 'search',
470
+ ...getBaseDataAttributes('searchRecentItem', 'search'),
478
471
  query,
479
472
  index: index.toString(),
480
473
  searchSessionId,
@@ -9,6 +9,7 @@ import { Button } from '@redocly/theme/components/Button/Button';
9
9
  import { useInputKeyCommands, useRecentSearches, useThemeHooks } from '@redocly/theme/core/hooks';
10
10
  import { CloseFilledIcon } from '@redocly/theme/icons/CloseFilledIcon/CloseFilledIcon';
11
11
  import { ChevronLeftIcon } from '@redocly/theme/icons/ChevronLeftIcon/ChevronLeftIcon';
12
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
12
13
 
13
14
  export type SearchInputProps = {
14
15
  placeholder?: string;
@@ -51,7 +52,9 @@ export function SearchInput({
51
52
  const handleOnReset = () => {
52
53
  onChange('');
53
54
  addSearchHistoryItem(value);
54
- telemetry.sendSearchInputResetButtonClickedMessage();
55
+ telemetry.sendSearchInputResetClickedMessage([
56
+ getBaseDataAttributes('searchInputReset', 'search'),
57
+ ]);
55
58
  };
56
59
 
57
60
  return (
@@ -3,7 +3,7 @@ import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
5
 
6
- import { breakpoints } from '@redocly/theme/core/utils';
6
+ import { breakpoints, getBaseDataAttributes } from '@redocly/theme/core/utils';
7
7
  import { useThemeHooks, useRecentSearches } from '@redocly/theme/core/hooks';
8
8
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
9
9
  import { RecentlyViewedIcon } from '@redocly/theme/icons/RecentlyViewedIcon/RecentlyViewedIcon';
@@ -25,7 +25,9 @@ export function SearchRecent({ onSelect, className }: SearchRecentProps): JSX.El
25
25
  const handleOnRemove = (e: React.MouseEvent<SVGSVGElement, MouseEvent>, item: string) => {
26
26
  e.stopPropagation();
27
27
  removeSearchHistoryItem(item);
28
- telemetry.sendSearchRecentRemoveButtonClickedMessage();
28
+ telemetry.sendSearchRecentRemoveClickedMessage([
29
+ getBaseDataAttributes('searchRecentRemove', 'search'),
30
+ ]);
29
31
  };
30
32
 
31
33
  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>, item: string, index: number) => {
@@ -11,6 +11,7 @@ import {
11
11
  ControlsWrapChangeLayoutButtons,
12
12
  } from '@redocly/theme/components/SidebarActions/styled';
13
13
  import { Tooltip } from '@redocly/theme/components/Tooltip/Tooltip';
14
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
14
15
 
15
16
  export { LayoutVariant };
16
17
 
@@ -57,9 +58,13 @@ export const SidebarActions = ({
57
58
  onClick={() => {
58
59
  onChangeCollapseSidebarClick();
59
60
  if (collapsedSidebar) {
60
- telemetry.sendSidebarItemExpandedMessage();
61
+ telemetry.sendSidebarExpandedMessage([
62
+ getBaseDataAttributes('sidebarExpand', 'button'),
63
+ ]);
61
64
  } else {
62
- telemetry.sendSidebarItemCollapsedMessage();
65
+ telemetry.sendSidebarCollapsedMessage([
66
+ getBaseDataAttributes('sidebarCollapse', 'button'),
67
+ ]);
63
68
  }
64
69
  }}
65
70
  size="small"
@@ -76,7 +81,9 @@ export const SidebarActions = ({
76
81
  layout={layout}
77
82
  onClick={() => {
78
83
  onChangeViewClick();
79
- telemetry.sendChangeLayoutButtonClickedMessage();
84
+ telemetry.sendChangeLayoutClickedMessage([
85
+ getBaseDataAttributes('changeLayout', 'button'),
86
+ ]);
80
87
  }}
81
88
  />
82
89
  </ControlsWrapChangeLayoutButtons>
@@ -10,7 +10,12 @@ import {
10
10
  useThemeConfig,
11
11
  useFullHeight,
12
12
  } from '@redocly/theme/core/hooks';
13
- import { breakpoints, getDisplayedHeadings, getLeastDepth } from '@redocly/theme/core/utils';
13
+ import {
14
+ breakpoints,
15
+ getDisplayedHeadings,
16
+ getLeastDepth,
17
+ getBaseDataAttributes,
18
+ } from '@redocly/theme/core/utils';
14
19
 
15
20
  export type TableOfContentProps = {
16
21
  headings?: Array<MdHeading | null> | null | undefined;
@@ -62,7 +67,7 @@ export function TableOfContent(props: TableOfContentProps): JSX.Element | null {
62
67
  data-testid={`toc-${heading.value}`}
63
68
  onClick={(e) => {
64
69
  e.preventDefault();
65
- telemetry.sendTocItemClickedMessage();
70
+ telemetry.sendTocItemClickedMessage([getBaseDataAttributes('tocItem', 'toc')]);
66
71
  handleHeadingClick(heading.id);
67
72
  }}
68
73
  />
@@ -4,6 +4,7 @@ import type { ButtonVariant, ButtonSize } from '@redocly/theme/components/Button
4
4
 
5
5
  import { useThemeHooks } from '@redocly/theme/core/hooks';
6
6
  import { Button } from '@redocly/theme/components/Button/Button';
7
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
7
8
 
8
9
  export type LoginButtonProps = {
9
10
  href: string;
@@ -36,7 +37,9 @@ export function LoginButton({
36
37
  data-translation-key={label ? undefined : labelTranslationKey}
37
38
  to={href}
38
39
  languageInsensitive
39
- onClick={() => telemetry.sendLoginButtonClickedMessage()}
40
+ onClick={() =>
41
+ telemetry.sendLoginClickedMessage([getBaseDataAttributes('login', 'button')])
42
+ }
40
43
  data-testid="login-btn"
41
44
  extraClass={className}
42
45
  variant={variant}
@@ -5,6 +5,7 @@ import type { JSX } from 'react';
5
5
  import { useThemeHooks } from '@redocly/theme/core/hooks';
6
6
  import { LogoutIcon } from '@redocly/theme/icons/LogoutIcon/LogoutIcon';
7
7
  import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
8
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
8
9
 
9
10
  export type LogoutMenuItemProps = {
10
11
  iconOnly?: boolean;
@@ -18,7 +19,7 @@ export function LogoutMenuItem({ iconOnly, className }: LogoutMenuItemProps): JS
18
19
  const { translate } = useTranslate();
19
20
 
20
21
  const handleClick = () => {
21
- telemetry.sendLogoutMenuItemClickedMessage();
22
+ telemetry.sendLogoutClickedMessage([getBaseDataAttributes('logoutItem', 'userMenu')]);
22
23
  handleLogout();
23
24
  };
24
25
 
@@ -5,6 +5,7 @@ import hotkeys from 'hotkeys-js';
5
5
  import { useThemeHooks } from '../use-theme-hooks';
6
6
  import { useThemeConfig } from '../use-theme-config';
7
7
  import { useSearchSession } from '../../contexts';
8
+ import { getBaseDataAttributes } from '../../utils';
8
9
 
9
10
  export function useSearchDialog() {
10
11
  const [isOpen, setIsOpen] = useState(false);
@@ -20,7 +21,12 @@ export function useSearchDialog() {
20
21
  if (hotKeys) {
21
22
  hotkeys(hotKeys, (ev) => {
22
23
  setIsOpen(true);
23
- telemetry.sendSearchOpenedMessage([{ object: 'search', method: 'shortcut' }]);
24
+ telemetry.sendSearchOpenedMessage([
25
+ {
26
+ ...getBaseDataAttributes('searchDialogId', 'search'),
27
+ method: 'shortcut',
28
+ },
29
+ ]);
24
30
  ev.preventDefault();
25
31
  });
26
32
 
@@ -30,7 +36,12 @@ export function useSearchDialog() {
30
36
  }, [hotKeys]);
31
37
 
32
38
  const onOpen = useCallback(function () {
33
- telemetry.sendSearchOpenedMessage([{ object: 'search', method: 'click' }]);
39
+ telemetry.sendSearchOpenedMessage([
40
+ {
41
+ ...getBaseDataAttributes('searchDialogId', 'search'),
42
+ method: 'click',
43
+ },
44
+ ]);
34
45
  setIsOpen(true);
35
46
  // eslint-disable-next-line react-hooks/exhaustive-deps
36
47
  }, []);
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
3
3
  import type { DisplayBanner } from '@redocly/theme/components/Banner/Banner';
4
4
 
5
5
  import { useThemeHooks } from './use-theme-hooks';
6
+ import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
6
7
 
7
8
  const noop = (): void => {};
8
9
  const noopLink = (_href: string): void => {};
@@ -28,11 +29,11 @@ export function useBannerTelemetry(
28
29
  };
29
30
  }
30
31
 
31
- const bannerUri = 'urn:redocly:realm:ui:banner:banner-id';
32
32
  const payload = {
33
- id: 'banner-id' as const,
34
- object: 'banner' as const,
35
- uri: bannerUri,
33
+ ...getBaseDataAttributes<'bannerId', 'banner', 'urn:redocly:realm:ui:banner:bannerId'>(
34
+ 'bannerId',
35
+ 'banner',
36
+ ),
36
37
  trackingId: displayBanner.trackingId,
37
38
  hash: displayBanner.hash,
38
39
  color: displayBanner.color,
@@ -6,6 +6,7 @@ import { useThemeConfig } from './use-theme-config';
6
6
  import { useThemeHooks } from './use-theme-hooks';
7
7
  import { createStore, useStore } from './use-store';
8
8
  import { DEFAULT_COLOR_MODES } from '../constants';
9
+ import { generateBeforeAfterContext } from '../utils';
9
10
 
10
11
  const COLOR_MODE_KEY = 'colorSchema';
11
12
  const colorModeStore = createStore<string>({
@@ -51,7 +52,12 @@ export const useColorSwitcher = () => {
51
52
  root.classList.remove('notransition');
52
53
  });
53
54
  telemetry.sendColorModeSwitchedMessage([
54
- { object: 'color_mode', from: activeColorMode, to: newMode },
55
+ ...generateBeforeAfterContext<'sendColorModeSwitchedMessage'>(
56
+ 'colorMode',
57
+ 'button',
58
+ { mode: activeColorMode },
59
+ { mode: newMode },
60
+ ),
55
61
  ]);
56
62
  setActiveColorMode(newMode);
57
63
  };
@@ -22,14 +22,7 @@ import {
22
22
  removeTrailingSlash,
23
23
  withoutPathPrefix,
24
24
  } from '../utils/urls';
25
-
26
- function createPageActionResource(pageSlug: string, pageUrl: string) {
27
- return {
28
- id: pageSlug,
29
- object: 'page' as const,
30
- uri: pageUrl,
31
- };
32
- }
25
+ import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
33
26
 
34
27
  const DEFAULT_ENABLED_ACTIONS = [
35
28
  'copy',
@@ -78,25 +71,22 @@ export function usePageActions(
78
71
 
79
72
  const isDocsMcp = !requiresMcpUrl;
80
73
 
81
- const origin = IS_BROWSER ? window.location.origin : '';
82
- const pageUrl = `${origin}${pageSlug}`;
83
-
84
74
  return createMCPAction({
85
75
  clientType,
86
76
  mcpConfig: config,
87
77
  translate,
88
78
  onClickCallback: isDocsMcp
89
79
  ? () =>
90
- telemetry.sendPageActionsButtonClickedMessage([
80
+ telemetry.sendPageActionsClickedMessage([
91
81
  {
92
- ...createPageActionResource(pageSlug, pageUrl),
93
- action_type: `docs-mcp-${clientType}` as const,
82
+ ...getBaseDataAttributes('pageActions', 'button'),
83
+ actionType: `docs-mcp-${clientType}` as const,
94
84
  },
95
85
  ])
96
86
  : undefined,
97
87
  });
98
88
  },
99
- [mcpUrl, mcpConfig, translate, telemetry, pageSlug],
89
+ [mcpUrl, mcpConfig, translate, telemetry],
100
90
  );
101
91
 
102
92
  const result: PageAction[] = useMemo(() => {
@@ -113,7 +103,6 @@ export function usePageActions(
113
103
  ? window.location.origin
114
104
  : ((globalThis as { SSR_HOSTNAME?: string })['SSR_HOSTNAME'] ?? '');
115
105
  const pathname = addTrailingSlash(pageSlug);
116
- const pageUrl = combineUrls(origin, pathname);
117
106
  const isRoot = withoutPathPrefix(pathname) === '/';
118
107
  const mdPageUrl = isRoot
119
108
  ? combineUrls(origin, pathname, 'index.html.md')
@@ -138,10 +127,10 @@ export function usePageActions(
138
127
  }
139
128
  const text = await result.text();
140
129
  ClipboardService.copyCustom(text);
141
- telemetry.sendPageActionsButtonClickedMessage([
130
+ telemetry.sendPageActionsClickedMessage([
142
131
  {
143
- ...createPageActionResource(pageSlug, pageUrl),
144
- action_type: 'copy',
132
+ ...getBaseDataAttributes('pageActions', 'button'),
133
+ actionType: 'copy',
145
134
  },
146
135
  ]);
147
136
  } catch (error) {
@@ -157,10 +146,10 @@ export function usePageActions(
157
146
  iconComponent: MarkdownFullIcon,
158
147
  link: mdPageUrl,
159
148
  onClick: () => {
160
- telemetry.sendPageActionsButtonClickedMessage([
149
+ telemetry.sendPageActionsClickedMessage([
161
150
  {
162
- ...createPageActionResource(pageSlug, pageUrl),
163
- action_type: 'view',
151
+ ...getBaseDataAttributes('pageActions', 'button'),
152
+ actionType: 'view',
164
153
  },
165
154
  ]);
166
155
  },
@@ -175,10 +164,10 @@ export function usePageActions(
175
164
  iconComponent: ChatGptIcon,
176
165
  link,
177
166
  onClick: () => {
178
- telemetry.sendPageActionsButtonClickedMessage([
167
+ telemetry.sendPageActionsClickedMessage([
179
168
  {
180
- ...createPageActionResource(pageSlug, pageUrl),
181
- action_type: 'chatgpt',
169
+ ...getBaseDataAttributes('pageActions', 'button'),
170
+ actionType: 'chatgpt',
182
171
  },
183
172
  ]);
184
173
  window.location.href = link;
@@ -195,10 +184,10 @@ export function usePageActions(
195
184
  iconComponent: ClaudeIcon,
196
185
  link,
197
186
  onClick: () => {
198
- telemetry.sendPageActionsButtonClickedMessage([
187
+ telemetry.sendPageActionsClickedMessage([
199
188
  {
200
- ...createPageActionResource(pageSlug, pageUrl),
201
- action_type: 'claude',
189
+ ...getBaseDataAttributes('pageActions', 'button'),
190
+ actionType: 'claude',
202
191
  },
203
192
  ]);
204
193
  window.location.href = link;
@@ -1,9 +1,10 @@
1
+ import { useRef } from 'react';
1
2
  import { useNavigate } from 'react-router-dom';
2
3
 
3
4
  import { getProductClassName } from '@redocly/theme/components/Product/utils';
4
5
 
5
6
  import { useThemeHooks } from './use-theme-hooks';
6
- import { withPathPrefix } from '../utils';
7
+ import { withPathPrefix, generateBeforeAfterContext } from '../utils';
7
8
 
8
9
  export function useProductPicker() {
9
10
  const { useCurrentProduct, useProducts, useTelemetry, useLoadAndNavigate } = useThemeHooks();
@@ -12,20 +13,33 @@ export function useProductPicker() {
12
13
  const telemetry = useTelemetry();
13
14
  const navigate = useNavigate();
14
15
  const loadAndNavigate = useLoadAndNavigate();
16
+ // An open dropdown can fire a menu item's onAction from an earlier render, so `product.link`
17
+ // may be stale (e.g. after a locale switch); resolve the live product via a ref.
18
+ const productsRef = useRef(products);
19
+ productsRef.current = products;
15
20
  function setProduct(product: typeof currentProduct) {
16
21
  if (!product) return;
17
- telemetry.sendProductPickedMessage([{ object: 'product', product: product.slug }]);
22
+ // match on `name` (locale-stable; link/slug are re-localized)
23
+ const freshProduct = productsRef.current.find((p) => p.name === product.name) ?? product;
24
+ telemetry.sendProductPickerChangedMessage([
25
+ ...generateBeforeAfterContext<'sendProductPickerChangedMessage'>(
26
+ 'productPicker',
27
+ 'dropdown',
28
+ { product: currentProduct?.slug || '' },
29
+ { product: freshProduct.slug },
30
+ ),
31
+ ]);
18
32
  if (typeof document === 'undefined') return;
19
33
 
20
- if (product.name) {
34
+ if (freshProduct.name) {
21
35
  const root = document.documentElement;
22
36
  Array.from(root.classList)
23
37
  .filter((c) => c.startsWith('product-'))
24
38
  .forEach((c) => root.classList.remove(c));
25
- root.classList.add(getProductClassName(product.name));
39
+ root.classList.add(getProductClassName(freshProduct.name));
26
40
  }
27
41
 
28
- loadAndNavigate({ navigate, to: withPathPrefix(product.link) });
42
+ loadAndNavigate({ navigate, to: withPathPrefix(freshProduct.link) });
29
43
  }
30
44
  return {
31
45
  currentProduct,
@@ -1,50 +1,61 @@
1
- // eslint-disable-next-line no-warning-comments
2
- // TODO we need to create some common way to do this.
1
+ // Fallback no-op telemetry used by `useThemeHooks` when no ThemeDataContext
2
+ // provider supplies real hooks.
3
3
  export const useTelemetryFallback = () => ({
4
4
  send: () => {},
5
- sendPageViewedMessage: () => {},
6
- sendPageActionsButtonClickedMessage: () => {},
7
5
  sendErrorMessage: () => {},
8
6
  sendClientErrorMessage: () => {},
7
+ sendCatalogEntitiesFilterCheckboxToggledMessage: () => {},
8
+ sendCatalogEntitiesCopyDataSchemaClickedMessage: () => {},
9
+ sendCatalogEntitiesViewModeChangedMessage: () => {},
10
+ sendCatalogEntitiesRelatedEntitiesListSearchQueryMessage: () => {},
11
+ sendCatalogEntitiesRelatedEntitiesListSearchResultClickMessage: () => {},
12
+ sendCatalogEntitiesListSearchQueryMessage: () => {},
13
+ sendCatalogEntitiesListSearchResultClickedMessage: () => {},
14
+ sendRedirectMessage: () => {},
15
+ sendCustomMessage: () => {},
16
+ sendSearchAIQueryMessage: () => {},
17
+ sendSearchAiOpenedMessage: () => {},
18
+ sendSearchAIFeedbackMessage: () => {},
19
+ sendLoginClickedMessage: () => {},
20
+ sendLoginProviderClickedMessage: () => {},
21
+ sendLogoutClickedMessage: () => {},
22
+ sendBannerViewedMessage: () => {},
23
+ sendBannerLinkClickedMessage: () => {},
24
+ sendBannerDismissedMessage: () => {},
9
25
  sendBreadcrumbClickedMessage: () => {},
10
- sendColorModeSwitchedMessage: () => {},
11
- sendSidebarItemClickedMessage: () => {},
12
- sendSidebarItemExpandedMessage: () => {},
13
- sendSidebarItemCollapsedMessage: () => {},
14
- sendChangeLayoutButtonClickedMessage: () => {},
15
- sendEditPageLinkClickedMessage: () => {},
16
- sendCodeSnippetReportedMessage: () => {},
17
- sendNavbarMenuItemClickedMessage: () => {},
18
- sendLoginButtonClickedMessage: () => {},
19
- sendLoginProviderButtonClickedMessage: () => {},
20
- sendLogoutMenuItemClickedMessage: () => {},
21
- sendLogoClickedMessage: () => {},
22
- sendTocItemClickedMessage: () => {},
26
+ sendCatalogActionsClickedMessage: () => {},
23
27
  sendCatalogFilterChangedMessage: () => {},
24
28
  sendCatalogItemClickedMessage: () => {},
25
29
  sendScorecardLinkClickedMessage: () => {},
26
- sendBackToCatalogButtonClickedMessage: () => {},
27
- sendSidebarDrilldownBackButtonClickedMessage: () => {},
30
+ sendFilterCheckboxToggledMessage: () => {},
31
+ sendFeedbackMessage: () => {},
28
32
  sendFooterItemClickedMessage: () => {},
29
- sendCatalogActionsButtonClickedMessage: () => {},
30
- sendMobileMenuButtonCloseClickedMessage: () => {},
31
- sendMobileMenuButtonOpenClickedMessage: () => {},
32
- sendSearchInputResetButtonClickedMessage: () => {},
33
- sendSearchRecentRemoveButtonClickedMessage: () => {},
33
+ sendNavbarMenuItemClickedMessage: () => {},
34
+ sendColorModeSwitchedMessage: () => {},
35
+ sendLanguagePickerChangedMessage: () => {},
36
+ sendProductPickerChangedMessage: () => {},
37
+ sendLogoClickedMessage: () => {},
38
+ sendCodeSnippetReportedMessage: () => {},
39
+ sendMobileMenuClosedMessage: () => {},
40
+ sendMobileMenuOpenedMessage: () => {},
41
+ sendPageViewedMessage: () => {},
42
+ sendPageTimeMessage: () => {},
43
+ sendPageActionsClickedMessage: () => {},
44
+ sendPageEditClickedMessage: () => {},
45
+ sendSidebarDrilldownBackClickedMessage: () => {},
46
+ sendSidebarItemClickedMessage: () => {},
47
+ sendSidebarExpandedMessage: () => {},
48
+ sendSidebarCollapsedMessage: () => {},
49
+ sendChangeLayoutClickedMessage: () => {},
50
+ sendRequestApiAccessClickedMessage: () => {},
51
+ sendVersionPickerChangedMessage: () => {},
52
+ sendSearchInputResetClickedMessage: () => {},
53
+ sendSearchResultClickedMessage: () => {},
54
+ sendSearchRecentRemoveClickedMessage: () => {},
34
55
  sendSearchRecentClickedMessage: () => {},
35
- sendRequestApiAccessButtonClickedMessage: () => {},
36
- sendVersionPickerSelectionChangeMessage: () => {},
37
- sendProductPickedMessage: () => {},
38
- sendFilterCheckboxToggledMessage: () => {},
39
- sendLanguagePickerLocaleChangedMessage: () => {},
40
56
  sendSearchOpenedMessage: () => {},
41
57
  sendSearchQueryMessage: () => {},
42
- sendSearchAiOpenedMessage: () => {},
43
- sendSearchAIQueryMessage: () => {},
44
- sendSearchAIFeedbackMessage: () => {},
45
- sendFeedbackMessage: () => {},
46
- sendSearchResultClickedMessage: () => {},
47
- sendRedirectMessage: () => {},
58
+ sendTocItemClickedMessage: () => {},
48
59
  sendOpenapiDocsMessage: () => {},
49
60
  sendCopyCodeSnippetClickedMessage: () => {},
50
61
  sendViewedMessage: () => {},
@@ -67,10 +78,7 @@ export const useTelemetryFallback = () => ({
67
78
  sendAsyncapiDocsReferencedInClickedMessage: () => {},
68
79
  sendGraphqlDocsViewedMessage: () => {},
69
80
  sendGraphqlDocsPerformanceMetricsMessage: () => {},
81
+ sendGraphqlDocsDownloadDefinitionClickedMessage: () => {},
70
82
  sendGraphqlDocsReferencedInLinkClickedMessage: () => {},
71
83
  sendGraphqlDocsRequiredScopesModalOpenedMessage: () => {},
72
- sendGraphqlDocsDownloadDefinitionClickedMessage: () => {},
73
- sendBannerViewedMessage: () => {},
74
- sendBannerLinkClickedMessage: () => {},
75
- sendBannerDismissedMessage: () => {},
76
84
  });
@@ -132,4 +132,5 @@ export enum ToolCallName {
132
132
  GetEndpointInfo = 'get-endpoint-info',
133
133
  GetSecuritySchemes = 'get-security-schemes',
134
134
  GetFullApiDescription = 'get-full-api-description',
135
+ Execute = 'execute',
135
136
  }
@@ -46,3 +46,5 @@ export * from './content-segments';
46
46
  export * from './custom-catalog-options-casing';
47
47
  export * from './get-auto-dismiss-duration';
48
48
  export * from './tooltip-placement';
49
+ export * from './telemetry/get-base-data-attributes';
50
+ export * from './telemetry/generate-before-after-context';
@@ -0,0 +1,59 @@
1
+ import type { AsyncApiRealmUI } from '@redocly/realm-asyncapi-sdk';
2
+
3
+ import { getBaseDataAttributes } from './get-base-data-attributes';
4
+
5
+ type UITelemetryClient = AsyncApiRealmUI.Telemetry;
6
+
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ type UITelemetryClientMethods = {
9
+ [K in keyof UITelemetryClient]: UITelemetryClient[K] extends (...args: any) => any
10
+ ? UITelemetryClient[K]
11
+ : never;
12
+ };
13
+ type UITelemetryClientMethodKeys = {
14
+ [K in keyof UITelemetryClientMethods]: Parameters<
15
+ UITelemetryClientMethods[K]
16
+ >[0] extends readonly [any, any, ...any[]]
17
+ ? K
18
+ : never;
19
+ }[keyof UITelemetryClientMethods];
20
+
21
+ type EventSchemaProps<T extends UITelemetryClientMethodKeys> = Parameters<
22
+ UITelemetryClientMethods[T]
23
+ >[0];
24
+
25
+ type EventAfterContext<T extends UITelemetryClientMethodKeys> = EventSchemaProps<T>[0];
26
+ type EventBeforeContext<T extends UITelemetryClientMethodKeys> = EventSchemaProps<T>[1];
27
+
28
+ /**
29
+ * Builds a typed `[after, before]` tuple for Realm UI telemetry events whose AsyncAPI
30
+ * contract lists the updated state first, then the prior state (per CloudEvents styleguide).
31
+ *
32
+ * Each slice merges {@link getBaseDataAttributes} (`id`, `object`, `uri`) with the
33
+ * corresponding partial payload and sets `context` to `'after'` or `'before'`.
34
+ *
35
+ * @template T - Telemetry client method key whose `data` tuple is
36
+ * `[after, before, ...]` (for example `'sendLanguagePickerChangedMessage'`).
37
+ * @param id - Stable resource identifier passed to {@link getBaseDataAttributes}
38
+ * and included on both payloads.
39
+ * @param object - Resource kind passed to {@link getBaseDataAttributes}
40
+ * and included on both payloads (for example `'dropdown'` or `'button'`).
41
+ * @param before - Event-specific fields for the prior state, excluding `id`, `object`,
42
+ * `uri`, and `context` (filled by this helper).
43
+ * @param after - Event-specific fields for the updated state, excluding `id`, `object`,
44
+ * `uri`, and `context` (filled by this helper).
45
+ * @returns A two-tuple in send order: the fully merged after payload, then the fully
46
+ * merged before payload. Spread into the matching `send*Message` call.
47
+ */
48
+ export function generateBeforeAfterContext<T extends UITelemetryClientMethodKeys>(
49
+ id: EventAfterContext<T>['id'],
50
+ object: EventAfterContext<T>['object'],
51
+ before: Omit<EventBeforeContext<T>, 'id' | 'object' | 'uri' | 'context'>,
52
+ after: Omit<EventAfterContext<T>, 'id' | 'object' | 'uri' | 'context'>,
53
+ ): [EventAfterContext<T> & { context: 'after' }, EventBeforeContext<T> & { context: 'before' }] {
54
+ const baseAttributes = getBaseDataAttributes(id, object);
55
+ return [
56
+ { ...baseAttributes, context: 'after' as const, ...after },
57
+ { ...baseAttributes, context: 'before' as const, ...before },
58
+ ];
59
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Generates a resource URN for Realm UI telemetry.
3
+ * @param object - The object type.
4
+ * @param id - The resource ID.
5
+ * @returns The resource URN.
6
+ */
7
+ export function generateResourceUrn<K, T, U>(object: K, id: T): U {
8
+ return `urn:redocly:realm:ui:${object}:${id}` as U;
9
+ }
@@ -0,0 +1,27 @@
1
+ import { generateResourceUrn } from './generate-resource-urn';
2
+
3
+ export type BaseDataAttributes<T, K, U> = {
4
+ /** Resource identifier (e.g. UI key, or page URL when tracking navigation). */
5
+ id: T;
6
+ /** Object kind used for telemetry categorization (e.g. `'button'`, `'page'`). */
7
+ object: K;
8
+ /** Resolved URI: the optional `url` when provided, otherwise a Realm UI resource URN. */
9
+ uri: U;
10
+ };
11
+
12
+ /**
13
+ * Builds the base fields for Realm UI telemetry payloads: `id`, `object`, and `uri`.
14
+ * When `url` is omitted, {@link generateResourceUrn} derives `uri` as
15
+ * `urn:redocly:realm:ui:{object}:{id}`.
16
+ */
17
+ export function getBaseDataAttributes<T, K, U>(
18
+ id: T,
19
+ object: K,
20
+ url?: U,
21
+ ): BaseDataAttributes<T, K, U> {
22
+ return {
23
+ id,
24
+ object,
25
+ uri: url ?? generateResourceUrn(object, id),
26
+ };
27
+ }