From f19c250d54e6ea19d4208a4f1845961db45818f5 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasevin Date: Tue, 25 Aug 2026 14:24:07 +0100 Subject: [PATCH] feat(builder): add network context to wizard, export and sidebar analytics events Audit of GA4 event call sites in the UI Builder: - wizard_step, export_clicked and sidebar_interaction now carry network_id and ecosystem alongside their existing params - every string dimension is normalised to "unknown" when missing/empty so event-scoped custom dimensions are never dropped - WizardLayout takes an optional analyticsContext prop; MainActions reads the selected network from the builder store - add component tests asserting exactly one event per user action - document the full event -> params table in ANALYTICS_FEATURE_FLAG.md --- apps/builder/ANALYTICS_FEATURE_FLAG.md | 30 ++++-- .../src/components/Common/WizardLayout.tsx | 8 +- .../__tests__/WizardLayout.analytics.test.tsx | 78 +++++++++++++++ .../Sidebar/AppSidebar/MainActions.tsx | 9 +- .../__tests__/MainActions.analytics.test.tsx | 93 ++++++++++++++++++ .../UIBuilder/hooks/useCompleteStepState.ts | 6 +- .../src/components/UIBuilder/index.tsx | 4 + .../__tests__/useBuilderAnalytics.test.ts | 94 +++++++++++++++++-- apps/builder/src/hooks/useBuilderAnalytics.ts | 90 ++++++++++++------ 9 files changed, 365 insertions(+), 47 deletions(-) create mode 100644 apps/builder/src/components/Common/__tests__/WizardLayout.analytics.test.tsx create mode 100644 apps/builder/src/components/Sidebar/AppSidebar/__tests__/MainActions.analytics.test.tsx diff --git a/apps/builder/ANALYTICS_FEATURE_FLAG.md b/apps/builder/ANALYTICS_FEATURE_FLAG.md index a0493c926..8d0133108 100644 --- a/apps/builder/ANALYTICS_FEATURE_FLAG.md +++ b/apps/builder/ANALYTICS_FEATURE_FLAG.md @@ -93,13 +93,29 @@ The analytics feature flag controls: ### Tracked Events -When analytics is enabled, the following user interactions **within the UI Builder** are tracked: - -- **Ecosystem Selection**: When users select blockchain ecosystems (EVM, Solana, etc.) in the builder -- **Network Selection**: When users choose specific networks within ecosystems in the builder -- **Export Actions**: When users click the "Export" button to generate standalone applications -- **Wizard Progress**: Each step progression through the form builder wizard interface -- **Sidebar Interactions**: Import/Export button clicks in the storage sidebar of the builder +When analytics is enabled, the following GA4 events are sent from **within the UI Builder**. All +events are fired from `src/hooks/useBuilderAnalytics.ts` (builder-specific) or the shared +`useAnalytics` hook from `@openzeppelin/ui-react` (`page_view`, `network_selected`). + +Every string parameter is normalised to `"unknown"` when the value is missing or empty, so GA4 +event-scoped custom dimensions are never dropped for lack of a value. + +| Event | Parameters | Fires when | Call site | +| ---------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `page_view` | `page_title`, `page_location` (GA defaults) | Once on initial load, automatically by `gtag('config')`. No manual `trackPageView` calls. | `AnalyticsProvider` in `App.tsx` | +| `ecosystem_selected` | `ecosystem` | User picks an ecosystem in the chain selector, or a deep link changes the ecosystem | `ChainSelector.tsx`, `useBuilderLifecycle.ts` | +| `network_selected` | `network_id`, `ecosystem` | User picks a network, or a deep link resolves a network | `ChainSelector.tsx`, `useBuilderLifecycle.ts` | +| `wizard_step` | `step_number` (1-indexed), `step_name`, `network_id`, `ecosystem` | Once per Next/Back click; describes the step being entered | `Common/WizardLayout.tsx` | +| `export_clicked` | `export_type`, `network_id`, `ecosystem` | Once after an app export succeeds | `UIBuilder/hooks/useCompleteStepState.ts` | +| `sidebar_interaction` | `action` (`import` \| `export`), `network_id`, `ecosystem` | Once per sidebar Import/Export click | `Sidebar/AppSidebar/MainActions.tsx` | +| `transaction_executed` | `network_id`, `ecosystem`, `execution_method` | A transaction succeeds from the form preview | `StepFormCustomization/FormPreview.tsx` | +| `contract_ui_created` | `network_id`, `ecosystem`, `total_records` | Once when a new Contract UI record is first persisted by auto-save | `UIBuilder/hooks/builder/useAutoSave.ts` | +| `relayer_service_configured` | `network_id`, `ecosystem` | Once per mount when relayer URL, API key and relayer selection are all set | `StepFormCustomization/components/RelayerConfiguration/index.tsx` | +| `uikit_changed` | `network_id`, `ecosystem`, `uikit_name` | User selects a UI kit in builder settings | `StepFormCustomization/components/UiKitSettings.tsx` | +| `address_book_opened` | `network_id`, `ecosystem` | Once when the address book dialog opens (not on network changes while open) | `AddressBook/AddressBookDialog.tsx` | + +GA4 event-scoped custom dimensions to register: `ecosystem`, `network_id`, `step_name`, +`step_number`, `export_type`, `action`, `execution_method`, `uikit_name`, `total_records`. **Important:** These analytics only track user behavior within the builder tool itself. The standalone applications that users export do not contain any analytics tracking. diff --git a/apps/builder/src/components/Common/WizardLayout.tsx b/apps/builder/src/components/Common/WizardLayout.tsx index 3d2fe8216..c57d56193 100644 --- a/apps/builder/src/components/Common/WizardLayout.tsx +++ b/apps/builder/src/components/Common/WizardLayout.tsx @@ -4,6 +4,7 @@ import React, { ReactNode } from 'react'; import { Button } from '@openzeppelin/ui-components'; import { cn } from '@openzeppelin/ui-utils'; +import type { AnalyticsNetworkContext } from '../../hooks/useBuilderAnalytics'; import { useBuilderAnalytics } from '../../hooks/useBuilderAnalytics'; export interface WizardStep { @@ -20,6 +21,8 @@ interface WizardLayoutProps { isWidgetExpanded?: boolean; currentStepIndex: number; onStepChange: (index: number) => void; + /** Network context attached to `wizard_step` analytics events. */ + analyticsContext?: AnalyticsNetworkContext; } export function WizardLayout({ @@ -29,6 +32,7 @@ export function WizardLayout({ isWidgetExpanded = false, currentStepIndex, onStepChange, + analyticsContext, }: WizardLayoutProps) { const isFirstStep = currentStepIndex === 0; const isLastStep = currentStepIndex === steps.length - 1; @@ -43,7 +47,7 @@ export function WizardLayout({ const nextStep = steps[nextStepIndex]; // Track wizard step progression - trackWizardStep(nextStepIndex + 1, nextStep.id); // Step numbers are 1-indexed for analytics + trackWizardStep(nextStepIndex + 1, nextStep.id, analyticsContext); // Step numbers are 1-indexed for analytics onStepChange(nextStepIndex); }; @@ -54,7 +58,7 @@ export function WizardLayout({ const prevStep = steps[prevStepIndex]; // Track wizard step progression (going backwards) - trackWizardStep(prevStepIndex + 1, prevStep.id); // Step numbers are 1-indexed for analytics + trackWizardStep(prevStepIndex + 1, prevStep.id, analyticsContext); // Step numbers are 1-indexed for analytics onStepChange(prevStepIndex); } diff --git a/apps/builder/src/components/Common/__tests__/WizardLayout.analytics.test.tsx b/apps/builder/src/components/Common/__tests__/WizardLayout.analytics.test.tsx new file mode 100644 index 000000000..c3c316b16 --- /dev/null +++ b/apps/builder/src/components/Common/__tests__/WizardLayout.analytics.test.tsx @@ -0,0 +1,78 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { WizardLayout, type WizardStep } from '../WizardLayout'; + +const mockTrackWizardStep = vi.fn(); + +vi.mock('../../../hooks/useBuilderAnalytics', () => ({ + useBuilderAnalytics: () => ({ + trackWizardStep: mockTrackWizardStep, + }), +})); + +const steps: WizardStep[] = [ + { id: 'chain', title: 'Chain', component:
chain
}, + { id: 'contract', title: 'Contract', component:
contract
}, + { id: 'function', title: 'Function', component:
function
}, + { id: 'customize', title: 'Customize', component:
customize
}, + { id: 'complete', title: 'Complete', component:
complete
}, +]; + +const analyticsContext = { networkId: 'ethereum-mainnet', ecosystem: 'evm' }; + +describe('WizardLayout analytics', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('fires wizard_step exactly once per Next click with the entered step and context', () => { + const onStepChange = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: /next/i })); + + expect(mockTrackWizardStep).toHaveBeenCalledTimes(1); + expect(mockTrackWizardStep).toHaveBeenCalledWith(3, 'function', analyticsContext); + expect(onStepChange).toHaveBeenCalledWith(2); + }); + + it('fires wizard_step exactly once per Back click with the entered step and context', () => { + const onStepChange = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: /back/i })); + + expect(mockTrackWizardStep).toHaveBeenCalledTimes(1); + expect(mockTrackWizardStep).toHaveBeenCalledWith(3, 'function', analyticsContext); + expect(onStepChange).toHaveBeenCalledWith(2); + }); + + it('does not fire wizard_step on render or when no navigation happens', () => { + render(); + + expect(mockTrackWizardStep).not.toHaveBeenCalled(); + }); + + it('passes undefined context when none is provided so the hook applies fallbacks', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /next/i })); + + expect(mockTrackWizardStep).toHaveBeenCalledWith(3, 'function', undefined); + }); +}); diff --git a/apps/builder/src/components/Sidebar/AppSidebar/MainActions.tsx b/apps/builder/src/components/Sidebar/AppSidebar/MainActions.tsx index 1ec1bc7f3..2eded2344 100644 --- a/apps/builder/src/components/Sidebar/AppSidebar/MainActions.tsx +++ b/apps/builder/src/components/Sidebar/AppSidebar/MainActions.tsx @@ -13,6 +13,7 @@ import { SidebarButton } from '@openzeppelin/ui-components'; import { useContractUIStorage } from '../../../contexts/useContractUIStorage'; import { useBuilderAnalytics } from '../../../hooks/useBuilderAnalytics'; import { AddressBookDialog } from '../../AddressBook/AddressBookDialog'; +import { useUIBuilderStore } from '../../UIBuilder/hooks/useUIBuilderStore'; import { recordHasMeaningfulContent } from '../../UIBuilder/utils/meaningfulContent'; interface MainActionsProps { @@ -31,17 +32,21 @@ export default function MainActions({ }: MainActionsProps) { const { exportContractUIs, contractUIs } = useContractUIStorage(); const { trackSidebarInteraction } = useBuilderAnalytics(); + const analyticsContext = useUIBuilderStore((s) => ({ + networkId: s.selectedNetworkConfigId, + ecosystem: s.selectedEcosystem, + })); const handleExport = async () => { // Track sidebar export interaction - trackSidebarInteraction('export'); + trackSidebarInteraction('export', analyticsContext); await exportContractUIs(); // Export all configurations }; const handleImport = () => { // Track sidebar import interaction - trackSidebarInteraction('import'); + trackSidebarInteraction('import', analyticsContext); onShowImportDialog(); }; diff --git a/apps/builder/src/components/Sidebar/AppSidebar/__tests__/MainActions.analytics.test.tsx b/apps/builder/src/components/Sidebar/AppSidebar/__tests__/MainActions.analytics.test.tsx new file mode 100644 index 000000000..5301ff815 --- /dev/null +++ b/apps/builder/src/components/Sidebar/AppSidebar/__tests__/MainActions.analytics.test.tsx @@ -0,0 +1,93 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { Ecosystem } from '@openzeppelin/ui-types'; + +import { uiBuilderStore } from '../../../UIBuilder/hooks/uiBuilderStore'; +import MainActions from '../MainActions'; + +const mockTrackSidebarInteraction = vi.fn(); +const mockExportContractUIs = vi.fn().mockResolvedValue(undefined); + +vi.mock('../../../../hooks/useBuilderAnalytics', () => ({ + useBuilderAnalytics: () => ({ + trackSidebarInteraction: mockTrackSidebarInteraction, + }), +})); + +vi.mock('../../../../contexts/useContractUIStorage', () => ({ + useContractUIStorage: () => ({ + exportContractUIs: mockExportContractUIs, + contractUIs: [ + { + id: 'r1', + title: 'Saved UI', + ecosystem: 'evm', + networkId: 'ethereum-mainnet', + contractAddress: '0x0000000000000000000000000000000000000001', + functionId: 'transfer', + formConfig: { fields: [{ id: 'f1' }] }, + metadata: {}, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ], + }), +})); + +vi.mock('../../../AddressBook/AddressBookDialog', () => ({ + AddressBookDialog: () => null, +})); + +vi.mock('../../../UIBuilder/utils/meaningfulContent', () => ({ + recordHasMeaningfulContent: () => true, +})); + +describe('MainActions analytics', () => { + beforeEach(() => { + vi.clearAllMocks(); + uiBuilderStore.updateState(() => ({ + selectedNetworkConfigId: 'stellar-testnet', + selectedEcosystem: 'stellar' as Ecosystem, + })); + }); + + it('fires sidebar_interaction import exactly once with the selected network context', () => { + const onShowImportDialog = vi.fn(); + render(); + + fireEvent.click(screen.getByRole('button', { name: /import/i })); + + expect(mockTrackSidebarInteraction).toHaveBeenCalledTimes(1); + expect(mockTrackSidebarInteraction).toHaveBeenCalledWith('import', { + networkId: 'stellar-testnet', + ecosystem: 'stellar', + }); + expect(onShowImportDialog).toHaveBeenCalledTimes(1); + }); + + it('fires sidebar_interaction export exactly once with the selected network context', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /export/i })); + + expect(mockTrackSidebarInteraction).toHaveBeenCalledTimes(1); + expect(mockTrackSidebarInteraction).toHaveBeenCalledWith('export', { + networkId: 'stellar-testnet', + ecosystem: 'stellar', + }); + expect(mockExportContractUIs).toHaveBeenCalledTimes(1); + }); + + it('passes null network id when no network has been selected yet', () => { + uiBuilderStore.updateState(() => ({ selectedNetworkConfigId: null })); + render(); + + fireEvent.click(screen.getByRole('button', { name: /import/i })); + + expect(mockTrackSidebarInteraction).toHaveBeenCalledWith('import', { + networkId: null, + ecosystem: 'stellar', + }); + }); +}); diff --git a/apps/builder/src/components/UIBuilder/hooks/useCompleteStepState.ts b/apps/builder/src/components/UIBuilder/hooks/useCompleteStepState.ts index bc693d28b..c3d24ad25 100644 --- a/apps/builder/src/components/UIBuilder/hooks/useCompleteStepState.ts +++ b/apps/builder/src/components/UIBuilder/hooks/useCompleteStepState.ts @@ -77,7 +77,11 @@ export function useCompleteStepState() { ); // Track successful export action - trackExportAction('react-vite'); // Default export type - could be made dynamic based on template choice + // Default export type - could be made dynamic based on template choice + trackExportAction('react-vite', { + networkId: networkConfig.id, + ecosystem: networkConfig.ecosystem, + }); if (result.data instanceof Blob) { downloadBlob(result.data, result.fileName); diff --git a/apps/builder/src/components/UIBuilder/index.tsx b/apps/builder/src/components/UIBuilder/index.tsx index 7e7f71b8d..f2eba9427 100644 --- a/apps/builder/src/components/UIBuilder/index.tsx +++ b/apps/builder/src/components/UIBuilder/index.tsx @@ -309,6 +309,10 @@ export function UIBuilder() { isWidgetExpanded={state.isWidgetVisible} currentStepIndex={state.currentStepIndex} onStepChange={handleStepChange} + analyticsContext={{ + networkId: state.selectedNetworkConfigId, + ecosystem: state.selectedEcosystem, + }} /> diff --git a/apps/builder/src/hooks/__tests__/useBuilderAnalytics.test.ts b/apps/builder/src/hooks/__tests__/useBuilderAnalytics.test.ts index b551ee045..b6635459d 100644 --- a/apps/builder/src/hooks/__tests__/useBuilderAnalytics.test.ts +++ b/apps/builder/src/hooks/__tests__/useBuilderAnalytics.test.ts @@ -83,24 +83,61 @@ describe('useBuilderAnalytics', () => { }); describe('trackExportAction', () => { - it('should track export action event', () => { + it('should track export action event with network context', () => { + const { result } = renderHook(() => useBuilderAnalytics()); + + result.current.trackExportAction('react-vite', { + networkId: 'ethereum-mainnet', + ecosystem: 'evm', + }); + + expect(mockTrackEvent).toHaveBeenCalledWith('export_clicked', { + export_type: 'react-vite', + network_id: 'ethereum-mainnet', + ecosystem: 'evm', + }); + }); + + it('should fall back to unknown when context is missing', () => { const { result } = renderHook(() => useBuilderAnalytics()); result.current.trackExportAction('react-vite'); - expect(mockTrackEvent).toHaveBeenCalledWith('export_clicked', { export_type: 'react-vite' }); + expect(mockTrackEvent).toHaveBeenCalledWith('export_clicked', { + export_type: 'react-vite', + network_id: 'unknown', + ecosystem: 'unknown', + }); }); }); describe('trackWizardStep', () => { - it('should track wizard step progression', () => { + it('should track wizard step progression with network context', () => { const { result } = renderHook(() => useBuilderAnalytics()); - result.current.trackWizardStep(2, 'configure'); + result.current.trackWizardStep(2, 'configure', { + networkId: 'stellar-testnet', + ecosystem: 'stellar', + }); expect(mockTrackEvent).toHaveBeenCalledWith('wizard_step', { step_number: 2, step_name: 'configure', + network_id: 'stellar-testnet', + ecosystem: 'stellar', + }); + }); + + it('should normalise null/empty context values to unknown', () => { + const { result } = renderHook(() => useBuilderAnalytics()); + + result.current.trackWizardStep(1, 'select-chain', { networkId: null, ecosystem: '' }); + + expect(mockTrackEvent).toHaveBeenCalledWith('wizard_step', { + step_number: 1, + step_name: 'select-chain', + network_id: 'unknown', + ecosystem: 'unknown', }); }); @@ -116,12 +153,31 @@ describe('useBuilderAnalytics', () => { }); describe('trackSidebarInteraction', () => { - it('should track sidebar interaction event', () => { + it('should track sidebar interaction event with network context', () => { const { result } = renderHook(() => useBuilderAnalytics()); - result.current.trackSidebarInteraction('import'); + result.current.trackSidebarInteraction('import', { + networkId: 'polygon-mainnet', + ecosystem: 'evm', + }); - expect(mockTrackEvent).toHaveBeenCalledWith('sidebar_interaction', { action: 'import' }); + expect(mockTrackEvent).toHaveBeenCalledWith('sidebar_interaction', { + action: 'import', + network_id: 'polygon-mainnet', + ecosystem: 'evm', + }); + }); + + it('should fall back to unknown when context is missing', () => { + const { result } = renderHook(() => useBuilderAnalytics()); + + result.current.trackSidebarInteraction('export'); + + expect(mockTrackEvent).toHaveBeenCalledWith('sidebar_interaction', { + action: 'export', + network_id: 'unknown', + ecosystem: 'unknown', + }); }); it('should track different actions', () => { @@ -190,6 +246,30 @@ describe('useBuilderAnalytics', () => { }); }); + describe('unknown fallbacks', () => { + it('should never emit empty string dimensions', () => { + const { result } = renderHook(() => useBuilderAnalytics()); + + result.current.trackEcosystemSelection(''); + result.current.trackTransactionExecuted('', '', ''); + result.current.trackUiKitChanged('', 'evm', ''); + + expect(mockTrackEvent).toHaveBeenNthCalledWith(1, 'ecosystem_selected', { + ecosystem: 'unknown', + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(2, 'transaction_executed', { + network_id: 'unknown', + ecosystem: 'unknown', + execution_method: 'unknown', + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(3, 'uikit_changed', { + network_id: 'unknown', + ecosystem: 'evm', + uikit_name: 'unknown', + }); + }); + }); + describe('trackAddressBookOpened', () => { it('should track address_book_opened', () => { const { result } = renderHook(() => useBuilderAnalytics()); diff --git a/apps/builder/src/hooks/useBuilderAnalytics.ts b/apps/builder/src/hooks/useBuilderAnalytics.ts index 354517f6c..dc0172068 100644 --- a/apps/builder/src/hooks/useBuilderAnalytics.ts +++ b/apps/builder/src/hooks/useBuilderAnalytics.ts @@ -2,6 +2,32 @@ import { useMemo } from 'react'; import { useAnalytics } from '@openzeppelin/ui-react'; +/** + * GA4 rejects/ignores empty parameter values and `undefined` would silently drop the dimension, + * so every string dimension falls back to `'unknown'` to keep custom dimension reports complete. + */ +const UNKNOWN = 'unknown'; + +function orUnknown(value: string | null | undefined): string { + return value && value.trim().length > 0 ? value : UNKNOWN; +} + +/** + * Network context shared by most builder events. Both values are optional at the call site + * (e.g. the wizard before a network is chosen) and normalised to `'unknown'` when missing. + */ +export interface AnalyticsNetworkContext { + networkId?: string | null; + ecosystem?: string | null; +} + +function networkParams(context: AnalyticsNetworkContext | undefined) { + return { + network_id: orUnknown(context?.networkId), + ecosystem: orUnknown(context?.ecosystem), + }; +} + /** * UI Builder-specific analytics hook. * Wraps the shared useAnalytics hook with builder-specific tracking events. @@ -33,32 +59,49 @@ export function useBuilderAnalytics() { * @param ecosystem - Selected ecosystem (e.g., 'evm', 'solana', 'stellar') */ trackEcosystemSelection: (ecosystem: string) => { - analytics.trackEvent('ecosystem_selected', { ecosystem }); + analytics.trackEvent('ecosystem_selected', { ecosystem: orUnknown(ecosystem) }); }, /** - * Track export action event. + * Fires once when an app export completes successfully. * @param exportType - Type of export (e.g., 'react-vite') + * @param context - Network the exported app targets */ - trackExportAction: (exportType: string) => { - analytics.trackEvent('export_clicked', { export_type: exportType }); + trackExportAction: (exportType: string, context?: AnalyticsNetworkContext) => { + analytics.trackEvent('export_clicked', { + export_type: orUnknown(exportType), + ...networkParams(context), + }); }, /** - * Track wizard step progression. - * @param stepNumber - Current step number - * @param stepName - Name/identifier of the step + * Fires once per Next/Back click in the wizard, describing the step being entered. + * @param stepNumber - 1-indexed step number being entered + * @param stepName - Name/identifier of the step being entered + * @param context - Network selected so far (`'unknown'` before chain selection) */ - trackWizardStep: (stepNumber: number, stepName: string) => { - analytics.trackEvent('wizard_step', { step_number: stepNumber, step_name: stepName }); + trackWizardStep: ( + stepNumber: number, + stepName: string, + context?: AnalyticsNetworkContext + ) => { + analytics.trackEvent('wizard_step', { + step_number: stepNumber, + step_name: orUnknown(stepName), + ...networkParams(context), + }); }, /** - * Track sidebar interaction event. + * Fires once per sidebar Import/Export click. * @param action - Action performed (e.g., 'import', 'export') + * @param context - Network currently selected in the builder */ - trackSidebarInteraction: (action: string) => { - analytics.trackEvent('sidebar_interaction', { action }); + trackSidebarInteraction: (action: string, context?: AnalyticsNetworkContext) => { + analytics.trackEvent('sidebar_interaction', { + action: orUnknown(action), + ...networkParams(context), + }); }, /** @@ -69,9 +112,8 @@ export function useBuilderAnalytics() { */ trackTransactionExecuted: (networkId: string, ecosystem: string, executionMethod: string) => { analytics.trackEvent('transaction_executed', { - network_id: networkId, - ecosystem, - execution_method: executionMethod, + ...networkParams({ networkId, ecosystem }), + execution_method: orUnknown(executionMethod), }); }, @@ -83,8 +125,7 @@ export function useBuilderAnalytics() { */ trackContractUiCreated: (networkId: string, ecosystem: string, totalRecords: number) => { analytics.trackEvent('contract_ui_created', { - network_id: networkId, - ecosystem, + ...networkParams({ networkId, ecosystem }), total_records: totalRecords, }); }, @@ -95,10 +136,7 @@ export function useBuilderAnalytics() { * @param ecosystem - Active ecosystem id */ trackRelayerServiceConfigured: (networkId: string, ecosystem: string) => { - analytics.trackEvent('relayer_service_configured', { - network_id: networkId, - ecosystem, - }); + analytics.trackEvent('relayer_service_configured', networkParams({ networkId, ecosystem })); }, /** @@ -109,9 +147,8 @@ export function useBuilderAnalytics() { */ trackUiKitChanged: (networkId: string, ecosystem: string, uikitName: string) => { analytics.trackEvent('uikit_changed', { - network_id: networkId, - ecosystem, - uikit_name: uikitName, + ...networkParams({ networkId, ecosystem }), + uikit_name: orUnknown(uikitName), }); }, @@ -121,10 +158,7 @@ export function useBuilderAnalytics() { * @param ecosystem - Active ecosystem id, or `'unknown'` */ trackAddressBookOpened: (networkId: string, ecosystem: string) => { - analytics.trackEvent('address_book_opened', { - network_id: networkId, - ecosystem, - }); + analytics.trackEvent('address_book_opened', networkParams({ networkId, ecosystem })); }, }), [analytics]