Skip to content

Commit e3dbca2

Browse files
authored
feat(sandbox): add segment analytics integration (#1344)
* segment integration
1 parent 291aea1 commit e3dbca2

19 files changed

Lines changed: 861 additions & 68 deletions

workspaces/sandbox/plugins/sandbox/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@mui/material": "^5.16.14",
5050
"@rhds/elements": "^2.0.1",
5151
"@rhds/icons": "^1.1.1",
52+
"@segment/analytics-next": "^1.0.0",
5253
"libphonenumber-js": "^1.12.8",
5354
"lodash": "^4.17.21",
5455
"react-phone-number-input": "^3.4.12",

workspaces/sandbox/plugins/sandbox/src/api/RegistrationBackendClient.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface RegistrationService {
3535
): Promise<void>;
3636
completePhoneVerification(code: string): Promise<void>;
3737
verifyActivationCode(code: string): Promise<void>;
38+
getSegmentWriteKey(): Promise<string>;
3839
}
3940

4041
export class RegistrationBackendClient implements RegistrationService {
@@ -192,4 +193,21 @@ export class RegistrationBackendClient implements RegistrationService {
192193
throw new Error(error?.message);
193194
}
194195
};
196+
197+
getSegmentWriteKey = async (): Promise<string> => {
198+
const signupAPI = this.configApi.getString('sandbox.signupAPI');
199+
const response = await this.secureFetchApi.fetch(
200+
`${signupAPI}/analytics/segment-write-key`,
201+
{
202+
method: 'GET',
203+
},
204+
);
205+
206+
if (!response.ok) {
207+
throw new Error(`Failed to fetch Segment write key: ${response.status}`);
208+
}
209+
210+
const writeKey = await response.text();
211+
return writeKey.trim();
212+
};
195213
}

workspaces/sandbox/plugins/sandbox/src/components/Modals/AnsibleLaunchInfoModal.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { Visibility, VisibilityOff } from '@mui/icons-material';
3838
import { Link } from '@backstage/core-components';
3939
import { useSandboxContext } from '../../hooks/useSandboxContext';
4040
import { AnsibleStatus } from '../../utils/aap-utils';
41-
import { pushCtaEvent } from '../../utils/eddl-utils';
41+
import { useTrackAnalytics } from '../../utils/eddl-utils';
4242
import { Intcmp } from '../../hooks/useProductURLs';
4343

4444
// Import the logos
@@ -64,14 +64,17 @@ export const AnsibleLaunchInfoModal: React.FC<AnsibleLaunchInfoModalProps> = ({
6464
ansibleStatus,
6565
} = useSandboxContext();
6666

67+
const trackAnalytics = useTrackAnalytics();
68+
6769
// Handle CTA click for analytics
68-
const handleAnsibleCtaClick = () => {
70+
const handleAnsibleCtaClick = async () => {
6971
if (ansibleUILink) {
70-
pushCtaEvent(
72+
await trackAnalytics(
7173
'Get Started - Ansible',
7274
'Catalog',
7375
ansibleUILink,
7476
Intcmp.AAP,
77+
'cta',
7578
);
7679
}
7780
};

workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/PhoneNumberStep.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ import IconButton from '@mui/material/IconButton';
3232
import CloseIcon from '@mui/icons-material/Close';
3333

3434
import CircularProgress from '@mui/material/CircularProgress';
35-
import { getEddlDataAttributes } from '../../../utils/eddl-utils';
35+
import {
36+
getEddlDataAttributes,
37+
useTrackAnalytics,
38+
} from '../../../utils/eddl-utils';
3639

3740
const FLAG_FETCH_URL =
3841
'https://catamphetamine.github.io/country-flag-icons/3x2';
@@ -65,6 +68,7 @@ export const PhoneNumberStep: React.FC<PhoneNumberFormProps> = ({
6568
error,
6669
}) => {
6770
const theme = useTheme();
71+
const trackAnalytics = useTrackAnalytics();
6872
const sendCodeEddlAttributes = getEddlDataAttributes(
6973
'Send Code',
7074
'Verification',
@@ -74,6 +78,22 @@ export const PhoneNumberStep: React.FC<PhoneNumberFormProps> = ({
7478
'Verification',
7579
);
7680

81+
// Handle Send Code click for analytics tracking
82+
const handleSendCodeClick = async () => {
83+
await trackAnalytics('Send Code', 'Verification', window.location.href);
84+
handlePhoneNumberSubmit();
85+
};
86+
87+
// Handle Cancel click for analytics tracking
88+
const handleCancelClick = async () => {
89+
await trackAnalytics(
90+
'Cancel Verification',
91+
'Verification',
92+
window.location.href,
93+
);
94+
handleClose();
95+
};
96+
7797
const PhoneInputField = forwardRef(function PhoneInputField(
7898
props: TextFieldProps,
7999
ref: ForwardedRef<React.ComponentType<TextFieldProps>>,
@@ -213,7 +233,7 @@ export const PhoneNumberStep: React.FC<PhoneNumberFormProps> = ({
213233
<Button
214234
data-testid="submit-phone-button"
215235
variant="contained"
216-
onClick={handlePhoneNumberSubmit}
236+
onClick={handleSendCodeClick}
217237
type="submit"
218238
disabled={!phoneNumber || loading}
219239
endIcon={
@@ -226,7 +246,7 @@ export const PhoneNumberStep: React.FC<PhoneNumberFormProps> = ({
226246
<Button
227247
data-testid="close-phone-button"
228248
variant="outlined"
229-
onClick={handleClose}
249+
onClick={handleCancelClick}
230250
sx={{
231251
border: `1px solid ${theme.palette.primary.main}`,
232252
'&:hover': {

workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/VerificationCodeStep.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ import { useSandboxContext } from '../../../hooks/useSandboxContext';
3939
import { Country, getCountryCallingCode } from 'react-phone-number-input';
4040
import { useApi } from '@backstage/core-plugin-api';
4141
import { registerApiRef } from '../../../api';
42-
import { getEddlDataAttributes } from '../../../utils/eddl-utils';
42+
import {
43+
getEddlDataAttributes,
44+
useTrackAnalytics,
45+
} from '../../../utils/eddl-utils';
4346

4447
type VerificationCodeProps = {
4548
id: Product;
@@ -69,6 +72,7 @@ export const VerificationCodeStep: React.FC<VerificationCodeProps> = ({
6972
setLoading,
7073
}) => {
7174
const theme = useTheme();
75+
const trackAnalytics = useTrackAnalytics();
7276
const startTrialEddlAttributes = getEddlDataAttributes(
7377
'Start Trial',
7478
'Verification',
@@ -161,7 +165,7 @@ export const VerificationCodeStep: React.FC<VerificationCodeProps> = ({
161165
setVerificationCodeError(undefined);
162166
setLoading(true);
163167
await registerApi.completePhoneVerification(otp.join(''));
164-
const maxAttempts = 5;
168+
const maxAttempts = 60;
165169
const retryInterval = 1000; // 1 second
166170

167171
// Poll until user is found or max attempts reached
@@ -216,6 +220,28 @@ export const VerificationCodeStep: React.FC<VerificationCodeProps> = ({
216220
}
217221
};
218222

223+
// Handle Start Trial click for analytics tracking
224+
const handleStartTrialClickWithTracking = async (pdt: Product) => {
225+
await trackAnalytics('Start Trial', 'Verification', window.location.href);
226+
handleStartTrialClick(pdt);
227+
};
228+
229+
// Handle Resend Code click for analytics tracking
230+
const handleResendCodeClickWithTracking = async () => {
231+
await trackAnalytics('Resend Code', 'Verification', window.location.href);
232+
handleResendCode();
233+
};
234+
235+
// Handle Cancel click for analytics tracking
236+
const handleCancelVerificationClick = async () => {
237+
await trackAnalytics(
238+
'Cancel Verification',
239+
'Verification',
240+
window.location.href,
241+
);
242+
handleClose();
243+
};
244+
219245
return (
220246
<>
221247
<DialogTitle
@@ -297,7 +323,7 @@ export const VerificationCodeStep: React.FC<VerificationCodeProps> = ({
297323
backgroundColor: 'transparent !important',
298324
}}
299325
onClick={() => {
300-
handleResendCode();
326+
handleResendCodeClickWithTracking();
301327
inputRefs.current[0]?.focus();
302328
}}
303329
{...resendCodeEddlAttributes}
@@ -331,7 +357,7 @@ export const VerificationCodeStep: React.FC<VerificationCodeProps> = ({
331357
data-testid="submit-opt-button"
332358
variant="contained"
333359
type="submit"
334-
onClick={() => handleStartTrialClick(id)}
360+
onClick={() => handleStartTrialClickWithTracking(id)}
335361
disabled={otp.some(digit => !digit) || loading}
336362
endIcon={
337363
loading && <CircularProgress size={20} sx={{ color: '#AFAFAF' }} />
@@ -343,7 +369,7 @@ export const VerificationCodeStep: React.FC<VerificationCodeProps> = ({
343369
<Button
344370
data-testid="close-opt-button"
345371
variant="outlined"
346-
onClick={handleClose}
372+
onClick={handleCancelVerificationClick}
347373
sx={{
348374
border: `1px solid ${theme.palette.primary.main}`,
349375
'&:hover': {

workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/__tests__/PhoneNumberStep.test.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@
1515
*/
1616

1717
import React from 'react';
18-
import { fireEvent, render, screen } from '@testing-library/react';
18+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
1919
import { PhoneNumberStep } from '../PhoneNumberStep';
2020
import { E164Number } from 'libphonenumber-js/types';
2121
import { Country } from 'react-phone-number-input';
2222
import { parsePhoneNumber } from 'libphonenumber-js/min';
2323
import { PhoneNumber } from 'libphonenumber-js';
24+
import * as eddlUtils from '../../../../utils/eddl-utils';
25+
26+
// Mock the useTrackAnalytics hook
27+
jest.mock('../../../../utils/eddl-utils', () => ({
28+
...jest.requireActual('../../../../utils/eddl-utils'),
29+
useTrackAnalytics: jest.fn(),
30+
}));
2431

2532
// Mock the props interface if it's not exported from the component file
2633
declare module '../PhoneNumberStep' {
@@ -43,10 +50,16 @@ describe('PhoneNumberStep', () => {
4350
const mockHandleClose = jest.fn();
4451
const mockHandlePhoneNumberSubmit = jest.fn();
4552
const mockSetCountry = jest.fn();
53+
const mockTrackAnalytics = jest.fn();
4654
const phoneNumber = parsePhoneNumber(' 8 (800) 555-35-35 ', 'RU');
4755

4856
beforeEach(() => {
4957
jest.clearAllMocks();
58+
// Mock the useTrackAnalytics hook to return a mock function
59+
mockTrackAnalytics.mockResolvedValue(undefined); // Make it async
60+
(eddlUtils.useTrackAnalytics as jest.Mock).mockReturnValue(
61+
mockTrackAnalytics,
62+
);
5063
});
5164

5265
function renderComponent(inputPhoneNumber: PhoneNumber, error?: string) {
@@ -86,37 +99,49 @@ describe('PhoneNumberStep', () => {
8699
expect(closeButton).toHaveLength(1);
87100
});
88101

89-
test('should submit phone number when clicking send code button', () => {
102+
test('should submit phone number when clicking send code button', async () => {
90103
renderComponent(phoneNumber);
91104
// Find and click send code button
92105
const submitPhoneNumberButton = screen.getByRole('button', {
93106
name: /Send code/i,
94107
});
95108
fireEvent.click(submitPhoneNumberButton);
96-
expect(mockHandlePhoneNumberSubmit).toHaveBeenCalled();
109+
110+
// Wait for the async tracking call to complete
111+
await waitFor(() => {
112+
expect(mockHandlePhoneNumberSubmit).toHaveBeenCalled();
113+
});
97114
});
98115

99-
test('should show an error when phone number is invalid', () => {
116+
test('should show an error when phone number is invalid', async () => {
100117
const invalidPhoneNumber = parsePhoneNumber(' 8 (800) xxxx ', 'RU');
101118
renderComponent(invalidPhoneNumber, 'invalid phone number error');
102119
// Find and click send code
103120
const submitPhoneNumberButton = screen.getByRole('button', {
104121
name: /Send code/i,
105122
});
106123
fireEvent.click(submitPhoneNumberButton);
107-
// submit the phone number to backend
108-
expect(mockHandlePhoneNumberSubmit).toHaveBeenCalled();
124+
125+
// Wait for the async tracking call to complete
126+
await waitFor(() => {
127+
expect(mockHandlePhoneNumberSubmit).toHaveBeenCalled();
128+
});
129+
109130
// expect mock error from backend to be displayed
110131
expect(screen.getByText('invalid phone number error')).toBeInTheDocument();
111132
// submit button should be enabled so user can retry with new number
112133
expect(screen.getByText(/Send code/i).closest('button')).toBeEnabled();
113134
});
114135

115-
test('closes the modal when the close button is clicked', () => {
136+
test('closes the modal when the close button is clicked', async () => {
116137
renderComponent(phoneNumber);
117138
const closeButton = screen.getByRole('button', { name: /Cancel/i });
118139
fireEvent.click(closeButton);
119-
expect(mockHandleClose).toHaveBeenCalled();
140+
141+
// Wait for the async tracking call to complete
142+
await waitFor(() => {
143+
expect(mockHandleClose).toHaveBeenCalled();
144+
});
120145
});
121146

122147
test('should not have a default country when no country is provided', () => {

workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AnsibleLaunchInfoModal.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,17 @@ describe('AnsibleLaunchInfoModal', () => {
4949
const mockUseSandboxContext = useSandboxContext as jest.MockedFunction<
5050
typeof useSandboxContext
5151
>;
52-
const mockPushCtaEvent = jest.spyOn(eddlUtils, 'pushCtaEvent');
52+
const mockUseTrackAnalytics = jest.spyOn(eddlUtils, 'useTrackAnalytics');
53+
54+
const mockTrackAnalytics = jest.fn();
5355

5456
beforeEach(() => {
5557
jest.clearAllMocks();
5658
// Setup window.appEventData for tests
5759
(global as any).window = { appEventData: [] };
60+
61+
// Mock useTrackAnalytics to return our mock function
62+
mockUseTrackAnalytics.mockReturnValue(mockTrackAnalytics);
5863
});
5964

6065
const renderModal = (contextOverrides = {}) => {
@@ -290,11 +295,12 @@ describe('AnsibleLaunchInfoModal', () => {
290295
fireEvent.click(linkElement!);
291296

292297
// Should push CTA event with correct parameters
293-
expect(mockPushCtaEvent).toHaveBeenCalledWith(
298+
expect(mockTrackAnalytics).toHaveBeenCalledWith(
294299
'Get Started - Ansible',
295300
'Catalog',
296301
'https://ansible.example.com',
297302
Intcmp.AAP,
303+
'cta',
298304
);
299305
});
300306

@@ -320,7 +326,7 @@ describe('AnsibleLaunchInfoModal', () => {
320326
fireEvent.click(linkElement!);
321327

322328
// Should not push event when link is not available
323-
expect(mockPushCtaEvent).not.toHaveBeenCalled();
329+
expect(mockTrackAnalytics).not.toHaveBeenCalled();
324330
});
325331
});
326332
});

workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesCard.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import Typography from '@mui/material/Typography';
2020
import CardMedia from '@mui/material/CardMedia';
2121
import { useTheme } from '@mui/material/styles';
2222
import { Link } from '@backstage/core-components';
23-
import { getEddlDataAttributes } from '../../utils/eddl-utils';
23+
import {
24+
getEddlDataAttributes,
25+
useTrackAnalytics,
26+
} from '../../utils/eddl-utils';
2427

2528
type SandboxActivitiesCardProps = {
2629
article: {
@@ -35,10 +38,21 @@ export const SandboxActivitiesCard: React.FC<SandboxActivitiesCardProps> = ({
3538
article: { img, title, description, link },
3639
}) => {
3740
const theme = useTheme();
41+
const trackAnalytics = useTrackAnalytics();
3842
const eddlAttributes = getEddlDataAttributes(title, 'Activities');
3943

44+
// Handle activity click for analytics tracking
45+
const handleActivityClick = async () => {
46+
await trackAnalytics(title, 'Activities', link);
47+
};
48+
4049
return (
41-
<Link to={link} style={{ textDecoration: 'none' }} {...eddlAttributes}>
50+
<Link
51+
to={link}
52+
onClick={handleActivityClick}
53+
style={{ textDecoration: 'none' }}
54+
{...eddlAttributes}
55+
>
4256
<Card
4357
elevation={0}
4458
sx={{

0 commit comments

Comments
 (0)