Skip to content

Commit 5e91e17

Browse files
authored
feat(builder): Google Analytics events for builder flows (#375)
* feat(builder): add Google Analytics events for key builder flows Track transaction_executed via TransactionForm onTransactionSuccess, contract_ui_created on first IndexedDB save, relayer_service_configured, uikit_changed, and address_book_opened. Bump @openzeppelin/ui-renderer to ^1.2.0 and @openzeppelin/ui-types to ^1.13.0. * fix(builder): address PR review on GA analytics - Remove unused saveContractUI from useAutoSave deps - Use 'unknown' for missing network/ecosystem dimensions on contract_ui_created - Fire address_book_opened only on dialog open transition; stable dimensions - Document new track* helpers with JSDoc
1 parent f606b7a commit 5e91e17

12 files changed

Lines changed: 270 additions & 42 deletions

File tree

apps/builder/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@
3737
"@openzeppelin/adapter-stellar": "^1.1.0",
3838
"@openzeppelin/ui-components": "^1.7.0",
3939
"@openzeppelin/ui-react": "^1.2.0",
40-
"@openzeppelin/ui-renderer": "^1.1.1",
40+
"@openzeppelin/ui-renderer": "^1.2.0",
4141
"@openzeppelin/ui-storage": "^1.2.0",
4242
"@openzeppelin/ui-styles": "^1.0.0",
43-
"@openzeppelin/ui-types": "^1.12.0",
43+
"@openzeppelin/ui-types": "^1.13.0",
4444
"@openzeppelin/ui-utils": "^1.4.0",
4545
"@radix-ui/react-accordion": "^1.2.11",
4646
"@radix-ui/react-checkbox": "^1.3.2",

apps/builder/src/components/AddressBook/AddressBookDialog.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { toast } from 'sonner';
2-
import { useCallback, useMemo, useState } from 'react';
2+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
33

44
import {
55
Dialog,
@@ -15,6 +15,7 @@ import type { NetworkConfig } from '@openzeppelin/ui-types';
1515

1616
import { getAdapter, getEcosystemMetadata } from '../../core/ecosystemManager';
1717
import { useAllNetworks } from '../../hooks/useAllNetworks';
18+
import { useBuilderAnalytics } from '../../hooks/useBuilderAnalytics';
1819
import { db } from '../../storage/database';
1920

2021
const ECOSYSTEM_ADDRESS_PATH: Record<string, string> = {
@@ -32,6 +33,22 @@ export function AddressBookDialog({ open, onOpenChange }: AddressBookDialogProps
3233
const { activeNetworkConfig, activeAdapter } = useWalletState();
3334
const { networks } = useAllNetworks();
3435
const [filterNetworkIds, setFilterNetworkIds] = useState<string[]>([]);
36+
const { trackAddressBookOpened } = useBuilderAnalytics();
37+
const wasAddressBookOpenRef = useRef(false);
38+
39+
useEffect(() => {
40+
const wasOpen = wasAddressBookOpenRef.current;
41+
wasAddressBookOpenRef.current = open;
42+
43+
if (!open || wasOpen) {
44+
return;
45+
}
46+
47+
const networkId = activeNetworkConfig?.id ?? activeAdapter?.networkConfig.id ?? 'unknown';
48+
const ecosystem =
49+
activeAdapter?.networkConfig.ecosystem ?? activeNetworkConfig?.ecosystem ?? 'unknown';
50+
trackAddressBookOpened(networkId, ecosystem);
51+
}, [open, activeNetworkConfig, activeAdapter, trackAddressBookOpened]);
3552

3653
const widgetProps = useAddressBookWidgetProps(db, {
3754
networkId: activeNetworkConfig?.id,

apps/builder/src/components/UIBuilder/StepFormCustomization/FormPreview.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ContractFunction, ContractSchema } from '@openzeppelin/ui-types';
77

88
import { formSchemaFactory } from '../../../core/factories/FormSchemaFactory';
99
import type { BuilderFormConfig } from '../../../core/types/FormTypes';
10+
import { useBuilderAnalytics } from '../../../hooks/useBuilderAnalytics';
1011

1112
interface FormPreviewProps {
1213
formConfig: BuilderFormConfig;
@@ -26,6 +27,7 @@ export function FormPreview({ formConfig, functionDetails, contractSchema }: For
2627
} = useWalletState();
2728

2829
const { isConnected: isWalletConnected } = useDerivedAccountStatus();
30+
const { trackTransactionExecuted } = useBuilderAnalytics();
2931

3032
// Convert BuilderFormConfig to RenderFormSchema using the FormSchemaFactory
3133
const renderSchema = useMemo(() => {
@@ -77,6 +79,9 @@ export function FormPreview({ formConfig, functionDetails, contractSchema }: For
7779
contractSchema={contractSchema}
7880
isWalletConnected={isWalletConnected}
7981
executionConfig={formConfig.executionConfig}
82+
onTransactionSuccess={({ network_id, ecosystem, execution_method }) => {
83+
trackTransactionExecuted(network_id, ecosystem, execution_method);
84+
}}
8085
/>
8186
</CardContent>
8287
</Card>

apps/builder/src/components/UIBuilder/StepFormCustomization/__tests__/UiKitSettings.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ import type { AvailableUiKit, ContractAdapter } from '@openzeppelin/ui-types';
77

88
import { UiKitSettings } from '../components/UiKitSettings';
99

10+
vi.mock('../../../../hooks/useBuilderAnalytics', () => ({
11+
useBuilderAnalytics: () => ({
12+
trackUiKitChanged: vi.fn(),
13+
trackEcosystemSelection: vi.fn(),
14+
trackExportAction: vi.fn(),
15+
trackWizardStep: vi.fn(),
16+
trackSidebarInteraction: vi.fn(),
17+
trackPageView: vi.fn(),
18+
trackNetworkSelection: vi.fn(),
19+
isEnabled: () => true,
20+
initialize: vi.fn(),
21+
tagId: '',
22+
trackTransactionExecuted: vi.fn(),
23+
trackContractUiCreated: vi.fn(),
24+
trackRelayerServiceConfigured: vi.fn(),
25+
trackAddressBookOpened: vi.fn(),
26+
}),
27+
}));
28+
1029
// Mock the logger with partial mocking to keep other exports
1130
vi.mock('@openzeppelin/ui-utils', async (importOriginal) => {
1231
const actual = await importOriginal<typeof import('@openzeppelin/ui-utils')>();

apps/builder/src/components/UIBuilder/StepFormCustomization/components/RelayerConfiguration/index.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React, { useCallback, useEffect } from 'react';
1+
import React, { useCallback, useEffect, useRef } from 'react';
22
import { useWatch } from 'react-hook-form';
33

44
import type { RelayerDetails } from '@openzeppelin/ui-types';
55

6+
import { useBuilderAnalytics } from '../../../../../hooks/useBuilderAnalytics';
67
import type { RelayerConfigurationProps } from '../../types';
78
import {
89
RelayerCredentialsCard,
@@ -17,6 +18,9 @@ export function RelayerConfiguration({
1718
adapter,
1819
setValue,
1920
}: RelayerConfigurationProps): React.ReactElement {
21+
const { trackRelayerServiceConfigured } = useBuilderAnalytics();
22+
const hasTrackedConfiguredRef = useRef(false);
23+
2024
const { setupStep, setSetupStep, localControl, sessionApiKey } = useRelayerConfiguration({
2125
control,
2226
adapter,
@@ -77,6 +81,31 @@ export function RelayerConfiguration({
7781
}
7882
}, [selectedRelayerId, fetchedRelayers, setValue]);
7983

84+
useEffect(() => {
85+
if (hasTrackedConfiguredRef.current || !adapter) {
86+
return;
87+
}
88+
const fullyConfigured =
89+
relayerServiceUrl.trim().length > 0 &&
90+
!!sessionApiKey &&
91+
!!selectedRelayerId &&
92+
fetchedRelayers.length > 0;
93+
94+
if (!fullyConfigured) {
95+
return;
96+
}
97+
98+
hasTrackedConfiguredRef.current = true;
99+
trackRelayerServiceConfigured(adapter.networkConfig.id, adapter.networkConfig.ecosystem);
100+
}, [
101+
adapter,
102+
relayerServiceUrl,
103+
sessionApiKey,
104+
selectedRelayerId,
105+
fetchedRelayers.length,
106+
trackRelayerServiceConfigured,
107+
]);
108+
80109
const handleFetchRelayers = () => {
81110
clearError();
82111
setValue('selectedRelayer', '', { shouldValidate: true });

apps/builder/src/components/UIBuilder/StepFormCustomization/components/UiKitSettings.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '@openzeppelin/ui-types';
1616
import { logger } from '@openzeppelin/ui-utils';
1717

18+
import { useBuilderAnalytics } from '../../../../hooks/useBuilderAnalytics';
1819
import { type SelectableOption } from '../../../Common/OptionSelector';
1920
import { TitledSection } from '../../../Common/TitledSection';
2021
import { ResponsiveUiKitSelector } from '../ResponsiveUiKitSelector';
@@ -31,6 +32,7 @@ interface UiKitOption extends SelectableOption {
3132
}
3233

3334
export function UiKitSettings({ adapter, onUpdateConfig, currentConfig }: UiKitSettingsProps) {
35+
const { trackUiKitChanged } = useBuilderAnalytics();
3436
const [availableKits, setAvailableKits] = useState<AvailableUiKit[]>([]);
3537
const [selectedKitId, setSelectedKitId] = useState<UiKitName | null>(null);
3638
const [isLoading, setIsLoading] = useState<boolean>(true);
@@ -203,6 +205,11 @@ export function UiKitSettings({ adapter, onUpdateConfig, currentConfig }: UiKitS
203205
};
204206

205207
onUpdateConfig(newConfig);
208+
209+
const nc = adapter.networkConfig;
210+
if (nc) {
211+
trackUiKitChanged(nc.id, nc.ecosystem, id);
212+
}
206213
}}
207214
configContent={configContent}
208215
isLoading={isLoading}

apps/builder/src/components/UIBuilder/hooks/builder/useAutoSave.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ContractAdapter } from '@openzeppelin/ui-types';
77
import { logger } from '@openzeppelin/ui-utils';
88

99
import { useContractUIStorage } from '../../../../contexts/useContractUIStorage';
10+
import { useBuilderAnalytics } from '../../../../hooks/useBuilderAnalytics';
1011
import { useStorageOperations } from '../../../../hooks/useStorageOperations';
1112
import { contractUIStorage, ContractUIStorage, type ContractUIRecord } from '../../../../storage';
1213
import { uiBuilderStore, type UIBuilderState } from '../uiBuilderStore';
@@ -130,7 +131,8 @@ async function prepareRecordWithDefinition(
130131
}
131132

132133
export function useAutoSave(isLoadingSavedConfigRef: React.RefObject<boolean>): AutoSaveHookReturn {
133-
const { updateContractUI, saveContractUI } = useContractUIStorage();
134+
const { updateContractUI, contractUIs } = useContractUIStorage();
135+
const { trackContractUiCreated } = useBuilderAnalytics();
134136
const storageOperations = useStorageOperations();
135137
const { activeAdapter } = useWalletState();
136138

@@ -236,6 +238,14 @@ export function useAutoSave(isLoadingSavedConfigRef: React.RefObject<boolean>):
236238
autoSaveCache.updateConfigCache(newConfigId, configToSave);
237239

238240
logger.info('Auto-save: New record created', `ID: ${newConfigId}`);
241+
242+
const networkId =
243+
currentState.selectedNetworkConfigId ?? activeAdapter?.networkConfig.id ?? 'unknown';
244+
const ecosystem =
245+
currentState.selectedEcosystem ?? activeAdapter?.networkConfig.ecosystem ?? 'unknown';
246+
const totalRecords = (contractUIs?.length ?? 0) + 1;
247+
trackContractUiCreated(networkId, ecosystem, totalRecords);
248+
239249
return;
240250
}
241251

@@ -306,7 +316,14 @@ export function useAutoSave(isLoadingSavedConfigRef: React.RefObject<boolean>):
306316
}
307317
globalAutoSaveState.releaseLock();
308318
}
309-
}, [isLoadingSavedConfigRef, updateContractUI, saveContractUI, storageOperations]);
319+
}, [
320+
isLoadingSavedConfigRef,
321+
updateContractUI,
322+
storageOperations,
323+
activeAdapter,
324+
contractUIs,
325+
trackContractUiCreated,
326+
]);
310327

311328
// Update the ref whenever autoSave changes
312329
useEffect(() => {

apps/builder/src/export/__tests__/__snapshots__/ExportSnapshotTests.test.ts.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,8 @@ exports[`Export Snapshot Tests > evm Export Snapshots > should match snapshot fo
293293
"@openzeppelin/adapter-evm": "^1.1.0",
294294
"@openzeppelin/ui-components": "^1.7.0",
295295
"@openzeppelin/ui-react": "^1.2.0",
296-
"@openzeppelin/ui-renderer": "^1.1.1",
297-
"@openzeppelin/ui-types": "^1.12.0",
296+
"@openzeppelin/ui-renderer": "^1.2.0",
297+
"@openzeppelin/ui-types": "^1.13.0",
298298
"@openzeppelin/ui-utils": "^1.4.0",
299299
"@tanstack/react-query": "^5.0.0",
300300
"@wagmi/core": "^2.20.3",
@@ -609,8 +609,8 @@ exports[`Export Snapshot Tests > polkadot Export Snapshots > should match snapsh
609609
"@openzeppelin/adapter-polkadot": "^1.1.0",
610610
"@openzeppelin/ui-components": "^1.7.0",
611611
"@openzeppelin/ui-react": "^1.2.0",
612-
"@openzeppelin/ui-renderer": "^1.1.1",
613-
"@openzeppelin/ui-types": "^1.12.0",
612+
"@openzeppelin/ui-renderer": "^1.2.0",
613+
"@openzeppelin/ui-types": "^1.13.0",
614614
"@openzeppelin/ui-utils": "^1.4.0",
615615
"@tanstack/react-query": "^5.0.0",
616616
"@wagmi/core": "^2.20.3",

apps/builder/src/export/versions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ export const packageVersions = {
1212
'@openzeppelin/adapter-solana': '1.1.0',
1313
'@openzeppelin/adapter-stellar': '1.1.0',
1414
'@openzeppelin/ui-react': '1.2.0',
15-
'@openzeppelin/ui-renderer': '1.1.1',
15+
'@openzeppelin/ui-renderer': '1.2.0',
1616
'@openzeppelin/ui-storage': '1.2.0',
17-
'@openzeppelin/ui-types': '1.12.0',
17+
'@openzeppelin/ui-types': '1.13.0',
1818
'@openzeppelin/ui-components': '1.7.0',
1919
'@openzeppelin/ui-utils': '1.4.0',
2020
'@openzeppelin/ui-styles': '1.0.0',

apps/builder/src/hooks/__tests__/useBuilderAnalytics.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,72 @@ describe('useBuilderAnalytics', () => {
134134
expect(mockTrackEvent).toHaveBeenCalledTimes(3);
135135
});
136136
});
137+
138+
describe('trackTransactionExecuted', () => {
139+
it('should track transaction_executed with network, ecosystem, execution method', () => {
140+
const { result } = renderHook(() => useBuilderAnalytics());
141+
142+
result.current.trackTransactionExecuted('ethereum-mainnet', 'evm', 'relayer');
143+
144+
expect(mockTrackEvent).toHaveBeenCalledWith('transaction_executed', {
145+
network_id: 'ethereum-mainnet',
146+
ecosystem: 'evm',
147+
execution_method: 'relayer',
148+
});
149+
});
150+
});
151+
152+
describe('trackContractUiCreated', () => {
153+
it('should track contract_ui_created with total_records', () => {
154+
const { result } = renderHook(() => useBuilderAnalytics());
155+
156+
result.current.trackContractUiCreated('stellar-testnet', 'stellar', 4);
157+
158+
expect(mockTrackEvent).toHaveBeenCalledWith('contract_ui_created', {
159+
network_id: 'stellar-testnet',
160+
ecosystem: 'stellar',
161+
total_records: 4,
162+
});
163+
});
164+
});
165+
166+
describe('trackRelayerServiceConfigured', () => {
167+
it('should track relayer_service_configured', () => {
168+
const { result } = renderHook(() => useBuilderAnalytics());
169+
170+
result.current.trackRelayerServiceConfigured('polygon-mainnet', 'evm');
171+
172+
expect(mockTrackEvent).toHaveBeenCalledWith('relayer_service_configured', {
173+
network_id: 'polygon-mainnet',
174+
ecosystem: 'evm',
175+
});
176+
});
177+
});
178+
179+
describe('trackUiKitChanged', () => {
180+
it('should track uikit_changed with uikit_name', () => {
181+
const { result } = renderHook(() => useBuilderAnalytics());
182+
183+
result.current.trackUiKitChanged('ethereum-mainnet', 'evm', 'rainbowkit');
184+
185+
expect(mockTrackEvent).toHaveBeenCalledWith('uikit_changed', {
186+
network_id: 'ethereum-mainnet',
187+
ecosystem: 'evm',
188+
uikit_name: 'rainbowkit',
189+
});
190+
});
191+
});
192+
193+
describe('trackAddressBookOpened', () => {
194+
it('should track address_book_opened', () => {
195+
const { result } = renderHook(() => useBuilderAnalytics());
196+
197+
result.current.trackAddressBookOpened('stellar-testnet', 'stellar');
198+
199+
expect(mockTrackEvent).toHaveBeenCalledWith('address_book_opened', {
200+
network_id: 'stellar-testnet',
201+
ecosystem: 'stellar',
202+
});
203+
});
204+
});
137205
});

0 commit comments

Comments
 (0)