diff --git a/apps/learning-snippets/src/test/quantity-formatting/QuantityFormatPanel.test.tsx b/apps/learning-snippets/src/test/quantity-formatting/QuantityFormatPanel.test.tsx index 8089f78274..b296f9576d 100644 --- a/apps/learning-snippets/src/test/quantity-formatting/QuantityFormatPanel.test.tsx +++ b/apps/learning-snippets/src/test/quantity-formatting/QuantityFormatPanel.test.tsx @@ -10,6 +10,9 @@ import { QuantityFormatPanel } from "@itwin/quantity-formatting-react"; import { IModelApp } from "@itwin/core-frontend"; import type { FormatDefinition } from "@itwin/core-quantity"; // __PUBLISH_EXTRACT_END__ +// __PUBLISH_EXTRACT_START__ QuantityFormat.TelemetryExampleImports +import { TelemetryContextProvider } from "@itwin/quantity-formatting-react"; +// __PUBLISH_EXTRACT_END__ import { QuantityFormattingTestUtils } from "../../utils/QuantityFormattingTestUtils.js"; describe("Quantity formatting", () => { @@ -50,5 +53,48 @@ describe("Quantity formatting", () => { expect(screen.getByText("labels.type")).to.exist; }); }); + + describe("Telemetry", () => { + before(async function () { + await QuantityFormattingTestUtils.initialize(); + }); + + after(async function () { + await QuantityFormattingTestUtils.terminate(); + }); + + it("renders with telemetry tracking", async function () { + // __PUBLISH_EXTRACT_START__ QuantityFormat.TelemetryExample + const formatDefinition: FormatDefinition = { + precision: 4, + type: "Decimal", + composite: { + units: [{ name: "Units.M", label: "m" }], + }, + }; + + const handleFormatChange = (_newFormat: FormatDefinition) => { + // Handle format change + }; + + const handleFeatureUsed = (featureId: string) => { + // Send to your analytics service + console.log(`Feature used: ${featureId}`); + }; + + render( + + + , + ); + // __PUBLISH_EXTRACT_END__ + + expect(screen.getByText("labels.type")).to.exist; + }); + }); }); }); diff --git a/apps/test-viewer/src/components/quantity-formatting/FormatSetsTabPanel.tsx b/apps/test-viewer/src/components/quantity-formatting/FormatSetsTabPanel.tsx index ff104c841d..97c3aa2ee6 100644 --- a/apps/test-viewer/src/components/quantity-formatting/FormatSetsTabPanel.tsx +++ b/apps/test-viewer/src/components/quantity-formatting/FormatSetsTabPanel.tsx @@ -1,13 +1,15 @@ +; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import React from "react"; +import React, { useCallback } from "react"; import { Button, Flex, Text } from "@itwin/itwinui-react"; -import { FormatSetPanel, FormatSetSelector } from "@itwin/quantity-formatting-react"; +import { FormatSetPanel, FormatSetSelector, TelemetryContextProvider } from "@itwin/quantity-formatting-react"; import type { FormatSet } from "@itwin/ecschema-metadata"; +import type { UsageTrackedFeatures } from "@itwin/quantity-formatting-react"; import type { FormatManager } from "./FormatManager"; interface FormatSetsTabPanelProps { @@ -86,52 +88,58 @@ export const FormatSetsTabPanel: React.FC = ({ formatMa } }, [selectedFormatSet]); + const handleFeatureUsed = useCallback((feature: UsageTrackedFeatures) => { + console.log(`[QuantityFormatting] Feature used: ${feature}`); + }, []); + return ( - - - - - - - - - - - - {clonedSelectedFormatSet ? ( - - ) : ( - - - Select a format set to view details - - - )} - - - - Clear - - - Save - - - Set as Active - - - - - - + + + + + + + + + + + + + {clonedSelectedFormatSet ? ( + + ) : ( + + + Select a format set to view details + + + )} + + + + Clear + + + Save + + + Set as Active + + + + + + + ); }; diff --git a/apps/test-viewer/src/components/quantity-formatting/FormatTabPanel.tsx b/apps/test-viewer/src/components/quantity-formatting/FormatTabPanel.tsx index e23bdeb47a..e534acede5 100644 --- a/apps/test-viewer/src/components/quantity-formatting/FormatTabPanel.tsx +++ b/apps/test-viewer/src/components/quantity-formatting/FormatTabPanel.tsx @@ -1,14 +1,16 @@ +; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import React from "react"; +import React, { useCallback } from "react"; import { Flex, Text } from "@itwin/itwinui-react"; -import { FormatSelector, QuantityFormatPanel } from "@itwin/quantity-formatting-react"; +import { FormatSelector, QuantityFormatPanel, TelemetryContextProvider } from "@itwin/quantity-formatting-react"; import type { FormatDefinition, UnitsProvider } from "@itwin/core-quantity"; import type { FormatSet } from "@itwin/ecschema-metadata"; +import type { UsageTrackedFeatures } from "@itwin/quantity-formatting-react"; interface FormatTabPanelProps { activeFormatSet: FormatSet | undefined; @@ -28,35 +30,41 @@ export const FormatTabPanel: React.FC = ({ onListItemChange, onFormatChange, }) => { + const handleFeatureUsed = useCallback((feature: UsageTrackedFeatures) => { + console.log(`[QuantityFormatting] Feature used: ${feature}`); + }, []); + return ( - - - - + + + + + - - {formatDefinition ? ( - <> - {(formatDefinition.label || formatDefinition.description) && ( - - {formatDefinition.label && {formatDefinition.label}} - {formatDefinition.description && ( - - {formatDefinition.description} - - )} - - )} - - > - ) : ( - - - Select a format in the list to edit - - - )} - - + + {formatDefinition ? ( + <> + {(formatDefinition.label || formatDefinition.description) && ( + + {formatDefinition.label && {formatDefinition.label}} + {formatDefinition.description && ( + + {formatDefinition.description} + + )} + + )} + + > + ) : ( + + + Select a format in the list to edit + + + )} + + + ); }; diff --git a/change/@itwin-quantity-formatting-react-98471588-b918-4914-9910-613e5012705f.json b/change/@itwin-quantity-formatting-react-98471588-b918-4914-9910-613e5012705f.json new file mode 100644 index 0000000000..2f6ce11dce --- /dev/null +++ b/change/@itwin-quantity-formatting-react-98471588-b918-4914-9910-613e5012705f.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add TelemetryContextProvider to quantity formatting react", + "packageName": "@itwin/quantity-formatting-react", + "email": "50554904+hl662@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/itwin/quantity-formatting/README.md b/packages/itwin/quantity-formatting/README.md index ea1487cff9..afbf3c0272 100644 --- a/packages/itwin/quantity-formatting/README.md +++ b/packages/itwin/quantity-formatting/README.md @@ -426,6 +426,74 @@ render(); +## Telemetry + +The quantity formatting components support telemetry tracking for usage analytics. You can hook into feature usage events by wrapping your components with the `TelemetryContextProvider`. + +### Basic Setup + + +Telemetry example code + + + + +```tsx +import { TelemetryContextProvider } from "@itwin/quantity-formatting-react"; + +import { QuantityFormatPanel } from "@itwin/quantity-formatting-react"; +import { IModelApp } from "@itwin/core-frontend"; +import type { FormatDefinition } from "@itwin/core-quantity"; + +const formatDefinition: FormatDefinition = { + precision: 4, + type: "Decimal", + composite: { + units: [{ name: "Units.M", label: "m" }], + }, +}; + +const handleFormatChange = (_newFormat: FormatDefinition) => { + // Handle format change +}; + +const handleFeatureUsed = (featureId: string) => { + // Send to your analytics service + console.log(`Feature used: ${featureId}`); +}; + +render( + + + , +); +``` + + + + + +### Tracked Features + +The following features are tracked: + +| Feature ID | Description | +| --------------------------- | ---------------------------------- | +| `format-apply` | User applies format changes | +| `format-clear` | User clears/resets format changes | +| `advanced-options-expand` | User expands advanced options | +| `advanced-options-collapse` | User collapses advanced options | +| `format-set-select` | User selects a format set | +| `format-select` | User selects a format | +| `format-set-search` | User initiates a format set search | +| `format-search` | User initiates a format search | +| `unit-system-change` | User changes the unit system | +| `format-type-change` | User changes the format type | +| `unit-change` | User changes the unit | +| `precision-change` | User changes precision | + +Additional internal feature tracking includes toggles and changes for decimal separators, thousands separators, sign options, and various format traits. + ## Complete Example A comprehensive example showing how to use FormatSelector together with QuantityFormatPanel can be found in this repository's test-viewer, found in [QuantityFormatButton.tsx](https://github.com/iTwin/viewer-components-react/blob/master/apps/test-viewer/src/components/QuantityFormatButton.tsx). The [common workflow](#common-worfklow) in the section above walks through the component in pictures. diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatPanel.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatPanel.tsx index 0fc022c7a4..b72836cf3e 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatPanel.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatPanel.tsx @@ -8,6 +8,7 @@ import * as React from "react"; import { FormatType, parseFormatType } from "@itwin/core-quantity"; import { Divider, ExpandableBlock, Flex, Surface, Text } from "@itwin/itwinui-react"; import { useTranslation } from "../../useTranslation.js"; +import { useTelemetryContext } from "../../hooks/UseTelemetryContext.js"; import { AzimuthPrimaryChildren, AzimuthSecondaryChildren } from "./panels/Azimuth.js"; import { BearingPrimaryChildren, BearingSecondaryChildren } from "./panels/Bearing.js"; import { DecimalPrimaryChildren, DecimalSecondaryChildren } from "./panels/Decimal.js"; @@ -35,6 +36,13 @@ export function FormatPanel(props: FormatPanelProps) { const { formatProps, unitsProvider, onFormatChange, persistenceUnit } = props; const [isExpanded, setIsExpanded] = React.useState(false); const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); + + const handleToggleExpanded = React.useCallback(() => { + const newExpanded = !isExpanded; + onFeatureUsed(newExpanded ? "advanced-options-expand" : "advanced-options-collapse"); + setIsExpanded(newExpanded); + }, [isExpanded, onFeatureUsed]); const [primaryChildren, secondaryChildren] = React.useMemo(() => { const panelProps: PanelProps = { @@ -112,7 +120,7 @@ export function FormatPanel(props: FormatPanelProps) { size="small" styleType="borderless" isExpanded={isExpanded} - onToggle={() => setIsExpanded(!isExpanded)} + onToggle={handleToggleExpanded} > {secondaryChildren} diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSelector.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSelector.tsx index 281efa603e..889762edea 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSelector.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSelector.tsx @@ -6,6 +6,7 @@ import * as React from "react"; import { Flex, Input, List, ListItem, Text } from "@itwin/itwinui-react"; import { useTranslation } from "../../useTranslation.js"; +import { useTelemetryContext } from "../../hooks/UseTelemetryContext.js"; import type { FormatDefinition } from "@itwin/core-quantity"; import type { FormatSet } from "@itwin/ecschema-metadata"; @@ -32,6 +33,7 @@ export const FormatSelector: React.FC = ({ onListItemChange, }) => { const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const [searchTerm, setSearchTerm] = React.useState(""); // Prepare format entries @@ -65,6 +67,7 @@ export const FormatSelector: React.FC = ({ (key: string) => { const formatEntry = formatEntries.find(entry => entry.key === key); if (formatEntry) { + onFeatureUsed("format-select"); onListItemChange(formatEntry.formatDef, key); } else { Logger.logWarning(logCategory,`Format entry not found for key: ${key}`, { @@ -74,14 +77,18 @@ export const FormatSelector: React.FC = ({ }); } }, - [onListItemChange, formatEntries] + [onListItemChange, formatEntries, onFeatureUsed, activeFormatSet?.name] ); const handleSearchChange = React.useCallback( (event: React.ChangeEvent) => { - setSearchTerm(event.target.value); + const value = event.target.value; + if (value && !searchTerm) { + onFeatureUsed("format-search"); + } + setSearchTerm(value); }, - [] + [onFeatureUsed, searchTerm] ); return ( diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetPanel.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetPanel.tsx index 83a9a787ee..9e53c4cdd4 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetPanel.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetPanel.tsx @@ -7,6 +7,7 @@ import React from "react"; import { SvgHelpCircularHollow } from "@itwin/itwinui-icons-react"; import { ComboBox, Flex, IconButton, Input, Label, Text, Textarea } from "@itwin/itwinui-react"; import { useTranslation } from "../../useTranslation.js"; +import { useTelemetryContext } from "../../hooks/UseTelemetryContext.js"; import type { UnitSystemKey } from "@itwin/core-quantity"; import type { FormatSet } from "@itwin/ecschema-metadata"; @@ -33,6 +34,7 @@ export const FormatSetPanel: React.FC = ({ formatSet, edita const [description, setDescription] = React.useState(""); const [unitSystem, setUnitSystem] = React.useState("metric"); const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); // Generate unique IDs for form elements const labelInputId = React.useId(); @@ -85,6 +87,7 @@ export const FormatSetPanel: React.FC = ({ formatSet, edita const handleUnitSystemChange = React.useCallback((value: string) => { setUnitSystem(value); + onFeatureUsed("unit-system-change"); if (editable && onFormatSetChange) { const updatedFormatSet: FormatSet = { @@ -93,7 +96,7 @@ export const FormatSetPanel: React.FC = ({ formatSet, edita }; onFormatSetChange(updatedFormatSet); } - }, [editable, formatSet, onFormatSetChange]); + }, [editable, formatSet, onFormatSetChange, onFeatureUsed]); return ( diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetSelector.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetSelector.tsx index 611ea27717..8ac7ee0e70 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetSelector.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/FormatSetSelector.tsx @@ -6,6 +6,7 @@ import React from "react"; import { Badge, Flex, Input, List, ListItem, Text } from "@itwin/itwinui-react"; import { useTranslation } from "../../useTranslation.js"; +import { useTelemetryContext } from "../../hooks/UseTelemetryContext.js"; import type { FormatSet } from "@itwin/ecschema-metadata"; @@ -45,6 +46,7 @@ export const FormatSetSelector: React.FC = ({ }) => { const [searchTerm, setSearchTerm] = React.useState(""); const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); // Filter format sets based on search term const filteredFormatSets = React.useMemo(() => { @@ -60,17 +62,28 @@ export const FormatSetSelector: React.FC = ({ const handleFormatSetSelect = React.useCallback( (formatSet: FormatSet) => { + onFeatureUsed("format-set-select"); const key = formatSet.name || `formatSet-${formatSets.indexOf(formatSet)}`; onFormatSetChange(formatSet, key); }, - [onFormatSetChange, formatSets] + [onFormatSetChange, formatSets, onFeatureUsed] + ); + + const handleSearchChange = React.useCallback( + (value: string) => { + if (value && !searchTerm) { + onFeatureUsed("format-set-search"); + } + setSearchTerm(value); + }, + [onFeatureUsed, searchTerm] ); return ( setSearchTerm(e.currentTarget.value)} + onChange={(e) => handleSearchChange(e.currentTarget.value)} placeholder={translate("QuantityFormat:labels.searchFormatSets")} /> diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/QuantityFormatPanel.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/QuantityFormatPanel.tsx index 731e0b7ce2..8cffdb5a82 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/QuantityFormatPanel.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/QuantityFormatPanel.tsx @@ -7,6 +7,7 @@ import "./FormatPanel.scss"; import * as React from "react"; import { Button, Flex, Text } from "@itwin/itwinui-react"; import { useTranslation } from "../../useTranslation.js"; +import { useTelemetryContext } from "../../hooks/UseTelemetryContext.js"; import { FormatPanel } from "./FormatPanel.js"; import { FormatSample } from "./FormatSample.js"; @@ -34,6 +35,7 @@ export function QuantityFormatPanel(props: QuantityFormatPanelProps) { } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); // Clone the formatDefinition to work with internally const [clonedFormatDefinition, setClonedFormatDefinition] = React.useState(() => @@ -90,15 +92,17 @@ export function QuantityFormatPanel(props: QuantityFormatPanelProps) { [] ); - const handleSave = React.useCallback(() => { + const handleApply = React.useCallback(() => { + onFeatureUsed("format-apply"); onFormatChange && onFormatChange(clonedFormatDefinition); setSaveEnabled(false); - }, [onFormatChange, clonedFormatDefinition]); + }, [onFormatChange, clonedFormatDefinition, onFeatureUsed]); const handleClear = React.useCallback(() => { + onFeatureUsed("format-clear"); setClonedFormatDefinition({...formatDefinition}); setSaveEnabled(false); - }, [formatDefinition]); + }, [formatDefinition, onFeatureUsed]); return ( @@ -120,7 +124,7 @@ export function QuantityFormatPanel(props: QuantityFormatPanelProps) { {translate("QuantityFormat:labels.clear")} - + {translate("QuantityFormat:labels.apply")} diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/DecimalSeparator.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/DecimalSeparator.tsx index 9b3c6a2ba2..1f0ece21ee 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/DecimalSeparator.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/DecimalSeparator.tsx @@ -10,6 +10,7 @@ import { Format, FormatTraits } from "@itwin/core-quantity"; import { Label } from "@itwin/itwinui-react"; import { DecimalSeparatorSelector } from "./misc/DecimalSeparator.js"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; /** Properties of [[DecimalSeparator]] component. * @internal @@ -25,11 +26,13 @@ export interface DecimalSeparatorProps { export function DecimalSeparator(props: DecimalSeparatorProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const decimalSeparatorSelectorId = React.useId(); const handleDecimalSeparatorChange = React.useCallback( (decimalSeparator: string) => { + onFeatureUsed("decimal-separator-change"); let thousandSeparator = formatProps.thousandSeparator; // make sure 1000 and decimal separator do not match if ( @@ -54,7 +57,7 @@ export function DecimalSeparator(props: DecimalSeparatorProps) { }; onChange(newFormatProps); }, - [formatProps, onChange] + [formatProps, onChange, onFeatureUsed] ); return ( diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatPrecision.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatPrecision.tsx index 6db125189a..271b8d73d7 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatPrecision.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatPrecision.tsx @@ -9,6 +9,7 @@ import type { FormatDefinition } from "@itwin/core-quantity"; import { FormatType, parseFormatType } from "@itwin/core-quantity"; import { FractionPrecisionSelector } from "./misc/FractionPrecision.js"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import { Label } from "@itwin/itwinui-react"; import { DecimalPrecisionSelector } from "./misc/DecimalPrecision.js"; @@ -26,9 +27,18 @@ export interface FormatPrecisionProps { export function FormatPrecision(props: FormatPrecisionProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const precisionSelectorId = React.useId(); const formatType = parseFormatType(formatProps.type, "format"); + const handlePrecisionChange = React.useCallback( + (value: number) => { + onFeatureUsed("precision-change"); + onChange({ ...formatProps, precision: value }); + }, + [onChange, formatProps, onFeatureUsed] + ); + return ( @@ -37,13 +47,13 @@ export function FormatPrecision(props: FormatPrecisionProps) { {formatType === FormatType.Fractional ? ( onChange({ ...formatProps, precision: value })} + onChange={handlePrecisionChange} aria-labelledby={precisionSelectorId} /> ) : ( onChange({ ...formatProps, precision: value })} + onChange={handlePrecisionChange} aria-labelledby={precisionSelectorId} /> )} diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnitLabel.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnitLabel.tsx index 271815424b..eae3130c2a 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnitLabel.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnitLabel.tsx @@ -10,6 +10,7 @@ import { Format, FormatTraits, getTraitString } from "@itwin/core-quantity"; import type { SelectOption } from "@itwin/itwinui-react"; import { Checkbox, Label, LabeledSelect } from "@itwin/itwinui-react"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; /** Properties of [[UomSeparatorSelector]] component. * @internal @@ -25,6 +26,7 @@ interface UomSeparatorSelectorProps { export function UomSeparatorSelector(props: UomSeparatorSelectorProps) { const { formatProps, onFormatChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const separatorOptions: SelectOption[] = React.useMemo(() => { const uomDefaultEntries: SelectOption[] = [ @@ -54,7 +56,10 @@ export function UomSeparatorSelector(props: UomSeparatorSelectorProps) { label={translate("QuantityFormat:labels.labelSeparator")} options={separatorOptions} value={formatProps.uomSeparator ?? ""} - onChange={(value: string) => onFormatChange({ ...formatProps, uomSeparator: value })} + onChange={(value: string) => { + onFeatureUsed("uom-separator-change"); + onFormatChange({ ...formatProps, uomSeparator: value }); + }} size="small" displayStyle="inline" /> @@ -76,6 +81,7 @@ interface AppendUnitLabelProps { export function AppendUnitLabel(props: AppendUnitLabelProps) { const { formatProps, onFormatChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const appendUnitLabelId = React.useId(); const setFormatTrait = React.useCallback( @@ -121,7 +127,10 @@ export function AppendUnitLabel(props: AppendUnitLabelProps) { setFormatTrait(FormatTraits.ShowUnitLabel, e.target.checked)} + onChange={(e) => { + onFeatureUsed("append-unit-label-toggle"); + setFormatTrait(FormatTraits.ShowUnitLabel, e.target.checked); + }} /> ); diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnits.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnits.tsx index 43f68321e9..ad7584a091 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnits.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/FormatUnits.tsx @@ -9,6 +9,7 @@ import * as React from "react"; import { SvgHelpCircularHollow } from "@itwin/itwinui-icons-react"; import { IconButton, Input, Label, Select } from "@itwin/itwinui-react"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import { getUnitName } from "./misc/UnitDescr.js"; import type { @@ -215,6 +216,7 @@ export function FormatUnits(props: FormatUnitsProps) { const { initialFormat, persistenceUnit, unitsProvider, onUnitsChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const initialFormatRef = React.useRef(initialFormat); const [formatProps, setFormatProps] = React.useState(initialFormat); const compositeSpacerSelectorId = React.useId(); @@ -236,6 +238,7 @@ export function FormatUnits(props: FormatUnitsProps) { const handleUnitLabelChange = React.useCallback( (newLabel: string, index: number) => { + onFeatureUsed("unit-label-change"); if ( formatProps.composite && formatProps.composite.units.length > index && @@ -251,11 +254,12 @@ export function FormatUnits(props: FormatUnitsProps) { handleSetFormatProps(newFormatProps); } }, - [formatProps, handleSetFormatProps] + [formatProps, handleSetFormatProps, onFeatureUsed] ); const handleUnitChange = React.useCallback( (newUnit: string, index: number) => { + onFeatureUsed("unit-change"); const unitParts = newUnit.split(/:/); if (unitParts[0] === "REMOVEUNIT") { if (formatProps.composite && formatProps.composite.units.length > 1) { @@ -299,7 +303,7 @@ export function FormatUnits(props: FormatUnitsProps) { } } }, - [formatProps, handleSetFormatProps] + [formatProps, handleSetFormatProps, onFeatureUsed] ); const handleOnSpacerChange = React.useCallback( diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepDecimalPoint.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepDecimalPoint.tsx index e7e9c6871f..56e9bffb2e 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepDecimalPoint.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepDecimalPoint.tsx @@ -9,6 +9,7 @@ import type { FormatProps } from "@itwin/core-quantity"; import { Format, FormatTraits, getTraitString } from "@itwin/core-quantity"; import { Checkbox, Label } from "@itwin/itwinui-react"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import "../FormatPanel.scss"; /** Properties of [[KeepDecimalPoint]] component. @@ -25,6 +26,7 @@ export interface KeepDecimalPointProps { export function KeepDecimalPoint(props: KeepDecimalPointProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const keepDecimalPointId = React.useId(); const setFormatTrait = React.useCallback( @@ -65,7 +67,10 @@ export function KeepDecimalPoint(props: KeepDecimalPointProps) { formatProps, FormatTraits.KeepDecimalPoint )} - onChange={(e) => setFormatTrait(FormatTraits.KeepDecimalPoint, e.target.checked)} + onChange={(e) => { + onFeatureUsed("keep-decimal-point-toggle"); + setFormatTrait(FormatTraits.KeepDecimalPoint, e.target.checked); + }} /> ); diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepSingleZero.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepSingleZero.tsx index cb2e360995..4ae741b311 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepSingleZero.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/KeepSingleZero.tsx @@ -9,6 +9,7 @@ import type { FormatProps } from "@itwin/core-quantity"; import { Format, FormatTraits, getTraitString } from "@itwin/core-quantity"; import { Checkbox, Label } from "@itwin/itwinui-react"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import "../FormatPanel.scss"; /** Properties of [[KeepSingleZero]] component. @@ -25,6 +26,7 @@ export interface KeepSingleZeroProps { export function KeepSingleZero(props: KeepSingleZeroProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const keepSingleZeroId = React.useId(); const setFormatTrait = React.useCallback( @@ -65,7 +67,10 @@ export function KeepSingleZero(props: KeepSingleZeroProps) { formatProps, FormatTraits.KeepSingleZero )} - onChange={(e) => setFormatTrait(FormatTraits.KeepSingleZero, e.target.checked)} + onChange={(e) => { + onFeatureUsed("keep-single-zero-toggle"); + setFormatTrait(FormatTraits.KeepSingleZero, e.target.checked); + }} /> ); diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ShowTrailingZeros.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ShowTrailingZeros.tsx index fcf4f067f9..bcb3526fee 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ShowTrailingZeros.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ShowTrailingZeros.tsx @@ -8,6 +8,7 @@ import * as React from "react"; import type { FormatProps } from "@itwin/core-quantity"; import { Format, FormatTraits, getTraitString } from "@itwin/core-quantity"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import { Checkbox, Label } from "@itwin/itwinui-react"; /** Properties of [[ShowTrailingZeros]] component. @@ -24,6 +25,7 @@ export interface ShowTrailingZerosProps { export function ShowTrailingZeros(props: ShowTrailingZerosProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const showTrailZerosId = React.useId(); const setFormatTrait = React.useCallback( @@ -64,7 +66,10 @@ export function ShowTrailingZeros(props: ShowTrailingZerosProps) { formatProps, FormatTraits.TrailZeroes )} - onChange={(e) => setFormatTrait(FormatTraits.TrailZeroes, e.target.checked)} + onChange={(e) => { + onFeatureUsed("show-trailing-zeros-toggle"); + setFormatTrait(FormatTraits.TrailZeroes, e.target.checked); + }} /> ); diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/SignOption.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/SignOption.tsx index 8cd7181e20..cc2044cc91 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/SignOption.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/SignOption.tsx @@ -9,6 +9,7 @@ import type { FormatProps } from "@itwin/core-quantity"; import { parseShowSignOption, type ShowSignOption } from "@itwin/core-quantity"; import { SignOptionSelector } from "./misc/SignOption.js"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import { IconButton, Label } from "@itwin/itwinui-react"; import { SvgHelpCircularHollow } from "@itwin/itwinui-icons-react"; import "../FormatPanel.scss"; @@ -27,6 +28,7 @@ export interface SignOptionProps { export function SignOption(props: SignOptionProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const showSignOptionId = React.useId(); const showSignOption = React.useMemo( @@ -54,7 +56,10 @@ export function SignOption(props: SignOptionProps) { onChange({ ...formatProps, showSignOption: value })} + onChange={(value: ShowSignOption) => { + onFeatureUsed("sign-option-change"); + onChange({ ...formatProps, showSignOption: value }); + }} /> ); diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ThousandsSeparator.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ThousandsSeparator.tsx index 2a219653e3..c16f04f2c0 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ThousandsSeparator.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ThousandsSeparator.tsx @@ -10,6 +10,7 @@ import { Format, FormatTraits, getTraitString } from "@itwin/core-quantity"; import { Checkbox, IconButton, Label } from "@itwin/itwinui-react"; import { SvgHelpCircularHollow } from "@itwin/itwinui-icons-react"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import { ThousandsSelector } from "./misc/ThousandsSelector.js"; /** Properties of [[UseThousandsSeparator]] component. @@ -26,6 +27,7 @@ export interface UseThousandsSeparatorProps { export function UseThousandsSeparator(props: UseThousandsSeparatorProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const useThousandsId = React.useId(); @@ -57,9 +59,10 @@ export function UseThousandsSeparator(props: UseThousandsSeparatorProps) { const handleUseThousandsSeparatorChange = React.useCallback( (e: React.ChangeEvent) => { + onFeatureUsed("thousands-separator-toggle"); setFormatTrait(FormatTraits.Use1000Separator, e.target.checked); }, - [setFormatTrait] + [setFormatTrait, onFeatureUsed] ); const isFormatTraitSet = React.useCallback( @@ -99,6 +102,7 @@ export function ThousandsSeparatorSelector( ) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const thousandsSelectorId = React.useId(); @@ -111,6 +115,7 @@ export function ThousandsSeparatorSelector( const handleThousandSeparatorChange = React.useCallback( (thousandSeparator: string) => { + onFeatureUsed("thousands-separator-change"); let decimalSeparator = formatProps.decimalSeparator; // make sure 1000 and decimal separator do not match if (isFormatTraitSet(FormatTraits.Use1000Separator)) { @@ -124,7 +129,7 @@ export function ThousandsSeparatorSelector( decimalSeparator, }); }, - [formatProps, isFormatTraitSet] + [formatProps, isFormatTraitSet, onFeatureUsed, onChange] ); // Only show if the Use1000Separator trait is set diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ZeroEmpty.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ZeroEmpty.tsx index 69b84e703e..0ef312ac38 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ZeroEmpty.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/ZeroEmpty.tsx @@ -9,6 +9,7 @@ import type { FormatProps } from "@itwin/core-quantity"; import { Format, FormatTraits, getTraitString } from "@itwin/core-quantity"; import { Checkbox, Label } from "@itwin/itwinui-react"; import { useTranslation } from "../../../useTranslation.js"; +import { useTelemetryContext } from "../../../hooks/UseTelemetryContext.js"; import "../FormatPanel.scss"; /** Properties of [[ZeroEmpty]] component. @@ -25,6 +26,7 @@ export interface ZeroEmptyProps { export function ZeroEmpty(props: ZeroEmptyProps) { const { formatProps, onChange } = props; const { translate } = useTranslation(); + const { onFeatureUsed } = useTelemetryContext(); const zeroEmptyId = React.useId(); const setFormatTrait = React.useCallback( @@ -65,7 +67,10 @@ export function ZeroEmpty(props: ZeroEmptyProps) { formatProps, FormatTraits.ZeroEmpty )} - onChange={(e) => setFormatTrait(FormatTraits.ZeroEmpty, e.target.checked)} + onChange={(e) => { + onFeatureUsed("zero-empty-toggle"); + setFormatTrait(FormatTraits.ZeroEmpty, e.target.checked); + }} /> ); diff --git a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/misc/FormatType.tsx b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/misc/FormatType.tsx index 6386e0176f..cdc3a4df75 100644 --- a/packages/itwin/quantity-formatting/src/components/quantityformat/internal/misc/FormatType.tsx +++ b/packages/itwin/quantity-formatting/src/components/quantityformat/internal/misc/FormatType.tsx @@ -17,6 +17,7 @@ import { import type { SelectOption } from "@itwin/itwinui-react"; import { LabeledSelect } from "@itwin/itwinui-react"; import { useTranslation } from "../../../../useTranslation.js"; +import { useTelemetryContext } from "../../../../hooks/UseTelemetryContext.js"; /** Properties of [[FormatTypeSelector]] component. * @alpha @@ -106,9 +107,11 @@ interface FormatTypeOptionProps { */ export function FormatTypeOption(props: FormatTypeOptionProps) { const { formatProps, onChange } = props; + const { onFeatureUsed } = useTelemetryContext(); const handleFormatTypeChange = React.useCallback( (type: FormatType) => { + onFeatureUsed("format-type-change"); let precision: number | undefined; let stationOffsetSize: number | undefined; let scientificType: string | undefined; @@ -169,7 +172,7 @@ export function FormatTypeOption(props: FormatTypeOptionProps) { }; onChange(newFormatProps); }, - [formatProps, onChange] + [formatProps, onChange, onFeatureUsed] ); const formatType = parseFormatType(formatProps.type, "format"); diff --git a/packages/itwin/quantity-formatting/src/hooks/UseTelemetryContext.tsx b/packages/itwin/quantity-formatting/src/hooks/UseTelemetryContext.tsx new file mode 100644 index 0000000000..8ab4024be5 --- /dev/null +++ b/packages/itwin/quantity-formatting/src/hooks/UseTelemetryContext.tsx @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { createContext, useContext, useEffect, useRef, useState } from "react"; + +import type { PropsWithChildren } from "react"; + +/** + * Features that are tracked for usage analytics. + * @beta + */ +export type UsageTrackedFeatures = + // Main panel actions + | "advanced-options-expand" + | "advanced-options-collapse" + | "format-apply" + | "format-clear" + // Selector actions + | "format-set-select" + | "format-select" + | "format-set-search" + | "format-search" + // Format set panel + | "unit-system-change" + // Format type + | "format-type-change" + // Unit options + | "unit-change" + | "unit-label-change" + | "append-unit-label-toggle" + | "uom-separator-change" + // Precision + | "precision-change" + // Advanced options + | "decimal-separator-change" + | "thousands-separator-toggle" + | "thousands-separator-change" + | "sign-option-change" + | "show-trailing-zeros-toggle" + | "keep-decimal-point-toggle" + | "keep-single-zero-toggle" + | "zero-empty-toggle"; + +/** @internal */ +export interface TelemetryContext { + onFeatureUsed: (featureId: UsageTrackedFeatures) => void; +} + +const telemetryContext = createContext({ + onFeatureUsed: () => {}, +}); + +/** + * Props for the TelemetryContextProvider component. + * @beta + */ +export interface TelemetryContextProviderProps { + /** Callback that is invoked when a tracked feature is used. */ + onFeatureUsed?: (featureId: UsageTrackedFeatures) => void; +} + +/** + * Provides callbacks to log telemetry events for specific features. + * @beta + */ +export function TelemetryContextProvider({ onFeatureUsed, children }: PropsWithChildren) { + const onFeatureUsedRef = useRef(onFeatureUsed); + + useEffect(() => { + onFeatureUsedRef.current = onFeatureUsed; + }, [onFeatureUsed]); + + // Create a stable context value that never changes reference across re-renders. + // This prevents unnecessary re-renders of all context consumers while still + // allowing the callback prop to change (accessed via the ref). + const [value] = useState(() => ({ + onFeatureUsed: (feature: UsageTrackedFeatures) => { + onFeatureUsedRef.current && onFeatureUsedRef.current(feature); + }, + })); + + return {children}; +} + +/** + * Hook to access telemetry context for reporting feature usage. + * @internal + */ +export function useTelemetryContext() { + return useContext(telemetryContext); +} diff --git a/packages/itwin/quantity-formatting/src/quantity-formatting-react.ts b/packages/itwin/quantity-formatting/src/quantity-formatting-react.ts index 1cc7ce15a3..0adb31ae1d 100644 --- a/packages/itwin/quantity-formatting/src/quantity-formatting-react.ts +++ b/packages/itwin/quantity-formatting/src/quantity-formatting-react.ts @@ -18,3 +18,11 @@ export { FormatPanel } from "./components/quantityformat/FormatPanel.js"; export { FormatSelector } from "./components/quantityformat/FormatSelector.js"; export { FormatSetPanel } from "./components/quantityformat/FormatSetPanel.js"; export { FormatSetSelector } from "./components/quantityformat/FormatSetSelector.js"; + +// Export telemetry hooks and types +export { + TelemetryContextProvider, + useTelemetryContext, + UsageTrackedFeatures, + TelemetryContextProviderProps, +} from "./hooks/UseTelemetryContext.js"; diff --git a/packages/itwin/quantity-formatting/src/test/quantityformat/FormatPanel.test.tsx b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatPanel.test.tsx new file mode 100644 index 0000000000..dba8e779d3 --- /dev/null +++ b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatPanel.test.tsx @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { describe, beforeEach, it, vi, expect } from "vitest"; +import * as React from "react"; +import { render, screen } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; +import type { FormatDefinition, UnitProps, UnitsProvider } from "@itwin/core-quantity"; +import { FormatPanel } from "../../components/quantityformat/FormatPanel.js"; +import { TelemetryContextProvider } from "../../hooks/UseTelemetryContext.js"; +import { IModelApp } from "@itwin/core-frontend"; + +describe("FormatPanel", () => { + let unitsProvider: UnitsProvider; + let persistenceUnit: UnitProps; + + beforeEach(async () => { + unitsProvider = IModelApp.quantityFormatter.unitsProvider; + persistenceUnit = await unitsProvider.findUnitByName("Units.M"); + }); + + describe("telemetry", () => { + it("should report 'advanced-options-expand' when expanding advanced options", async () => { + const user = userEvent.setup(); + const onFeatureUsedSpy = vi.fn(); + const formatDefinition: FormatDefinition = { + type: "Decimal", + precision: 2, + composite: { + units: [{ name: "Units.M", label: "m" }], + }, + }; + + render( + + + + ); + + const expandableBlock = screen.getByText("labels.advancedOptions"); + await user.click(expandableBlock); + + expect(onFeatureUsedSpy).toHaveBeenCalledWith("advanced-options-expand"); + }); + + it("should report 'advanced-options-collapse' when collapsing advanced options", async () => { + const user = userEvent.setup(); + const onFeatureUsedSpy = vi.fn(); + const formatDefinition: FormatDefinition = { + type: "Decimal", + precision: 2, + composite: { + units: [{ name: "Units.M", label: "m" }], + }, + }; + + render( + + + + ); + + const expandableBlock = screen.getByText("labels.advancedOptions"); + + // First expand + await user.click(expandableBlock); + expect(onFeatureUsedSpy).toHaveBeenCalledWith("advanced-options-expand"); + + // Then collapse + await user.click(expandableBlock); + expect(onFeatureUsedSpy).toHaveBeenCalledWith("advanced-options-collapse"); + }); + }); +}); diff --git a/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSelector.test.tsx b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSelector.test.tsx index e027f64ad1..8712d898e9 100644 --- a/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSelector.test.tsx +++ b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSelector.test.tsx @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { FormatSelector } from "../../components/quantityformat/FormatSelector.js"; +import { TelemetryContextProvider } from "../../hooks/UseTelemetryContext.js"; import type { FormatDefinition } from "@itwin/core-quantity"; import type { FormatSet } from "@itwin/ecschema-metadata"; @@ -396,4 +397,40 @@ describe("FormatSelector", () => { }); }); }); + + describe("Telemetry", () => { + it("should report 'format-select' when a format is clicked", async () => { + const user = userEvent.setup(); + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + const lengthFormatItem = screen.getByText("Length Format"); + await user.click(lengthFormatItem); + + expect(onFeatureUsedSpy).toHaveBeenCalledWith("format-select"); + }); + + it("should report 'format-search' when search starts (fire once)", async () => { + const user = userEvent.setup(); + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + const searchInput = screen.getByPlaceholderText("QuantityFormat:labels.searchFormats"); + await user.type(searchInput, "Length"); + + // Should only fire once at the start of typing + expect(onFeatureUsedSpy).toHaveBeenCalledWith("format-search"); + expect(onFeatureUsedSpy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetPanel.test.tsx b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetPanel.test.tsx index d3c46b4fcb..7e529a2855 100644 --- a/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetPanel.test.tsx +++ b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetPanel.test.tsx @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { FormatSetPanel } from "../../components/quantityformat/FormatSetPanel.js"; +import { TelemetryContextProvider } from "../../hooks/UseTelemetryContext.js"; import type { FormatSet } from "@itwin/ecschema-metadata"; // Mock the useTranslation hook @@ -207,4 +208,50 @@ describe("FormatSetPanel", () => { expect(container.querySelector(".quantityFormat--formatSetPanel-inputRow-descr")).toBeTruthy(); }); }); + + describe("Telemetry", () => { + it("should report 'unit-system-change' when unit system is changed", async () => { + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + // Get the unit system select and open it + const unitSystemSelect = screen.queryAllByLabelText("Unit System")[1] as HTMLInputElement; + await user.click(unitSystemSelect); + + // Select a different unit system + const imperialOption = await screen.findByRole("option", { name: "Imperial" }); + await user.click(imperialOption); + + expect(onFeatureUsedSpy).toHaveBeenCalledWith("unit-system-change"); + }); + + it("should not report telemetry when unit system select is disabled", async () => { + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + // Unit system select should be disabled + const unitSystemSelect = screen.queryAllByLabelText("Unit System")[1] as HTMLInputElement; + expect(unitSystemSelect.disabled).toBe(true); + + // No telemetry should be reported + expect(onFeatureUsedSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetSelector.test.tsx b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetSelector.test.tsx index 2b196ceb9c..c1c16ee0c2 100644 --- a/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetSelector.test.tsx +++ b/packages/itwin/quantity-formatting/src/test/quantityformat/FormatSetSelector.test.tsx @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { FormatSetSelector } from "../../components/quantityformat/FormatSetSelector.js"; +import { TelemetryContextProvider } from "../../hooks/UseTelemetryContext.js"; import type { FormatSet } from "@itwin/ecschema-metadata"; // Mock the useTranslation hook @@ -425,4 +426,44 @@ describe("FormatSetSelector", () => { expect(screen.queryByText("Format Set 50")).toBeNull(); }); }); + + describe("Telemetry", () => { + it("should report 'format-set-select' when a format set is clicked", async () => { + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + const formatSetItem = screen.getByText("Arizona Highway Project (Civil)"); + await user.click(formatSetItem); + + expect(onFeatureUsedSpy).toHaveBeenCalledWith("format-set-select"); + }); + + it("should report 'format-set-search' when search starts (fire once)", async () => { + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + const searchInput = screen.getByPlaceholderText("Search format sets..."); + await user.type(searchInput, "Arizona"); + + // Should only fire once at the start of typing + expect(onFeatureUsedSpy).toHaveBeenCalledWith("format-set-search"); + expect(onFeatureUsedSpy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/itwin/quantity-formatting/src/test/quantityformat/QuantityFormatPanel.test.tsx b/packages/itwin/quantity-formatting/src/test/quantityformat/QuantityFormatPanel.test.tsx index 65d65a5035..41b690a74f 100644 --- a/packages/itwin/quantity-formatting/src/test/quantityformat/QuantityFormatPanel.test.tsx +++ b/packages/itwin/quantity-formatting/src/test/quantityformat/QuantityFormatPanel.test.tsx @@ -10,6 +10,7 @@ import { vi, describe, it, expect, beforeEach } from "vitest"; import type { FormatDefinition, UnitsProvider, UnitProps } from "@itwin/core-quantity"; import { QuantityFormatPanel } from "../../components/quantityformat/QuantityFormatPanel.js"; import { QuantityFormatting } from "../../QuantityFormatting.js"; +import { TelemetryContextProvider } from "../../hooks/UseTelemetryContext.js"; // Mock the useTranslation hook vi.mock("../../useTranslation.js", () => ({ @@ -234,4 +235,75 @@ describe("QuantityFormatPanel", () => { expect(screen.getByTestId("format-sample")).toBeDefined(); }); + + describe("telemetry", () => { + it("should report 'format-apply' when Apply button is clicked", async () => { + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + const applyButton = screen.getByRole("button", { name: "Apply" }); + const triggerChangeButton = screen.getByTestId("trigger-format-change"); + + // Trigger format change to enable Apply button + await user.click(triggerChangeButton); + // Click Apply button + await user.click(applyButton); + + expect(onFeatureUsedSpy).toHaveBeenCalledWith("format-apply"); + }); + + it("should report 'format-clear' when Clear button is clicked", async () => { + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + const clearButton = screen.getByRole("button", { name: "Clear" }); + const triggerChangeButton = screen.getByTestId("trigger-format-change"); + + // Trigger format change to enable Clear button + await user.click(triggerChangeButton); + // Click Clear button + await user.click(clearButton); + + expect(onFeatureUsedSpy).toHaveBeenCalledWith("format-clear"); + }); + + it("should not report telemetry when Apply button is disabled", async () => { + const onFeatureUsedSpy = vi.fn(); + + render( + + + + ); + + const applyButton = screen.getByRole("button", { name: "Apply" }); + + // Click disabled Apply button + await user.click(applyButton); + + expect(onFeatureUsedSpy).not.toHaveBeenCalled(); + }); + }); });