Skip to content

Commit cce9487

Browse files
feat(EventBuilder): enable EU data collection (#2236)
* add initial functionality * add useEuEndpoint to callback dependency list * refactor implementation to move switch to ValidateEvent/index.tsx * add tests and improve formatting * fix linting errors * cleanup unused style code * remove added comma * use correct naming standard * fix styling in validate event * Update src/components/ga4/EventBuilder/ValidateEvent/index.tsx Co-authored-by: Josh Radcliff <jradcliff@users.noreply.github.com> --------- Co-authored-by: Josh Radcliff <jradcliff@users.noreply.github.com>
1 parent 8ef6d7d commit cce9487

File tree

5 files changed

+224
-86
lines changed

5 files changed

+224
-86
lines changed

src/components/WithHelpText.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const Root = styled('div')((
7878
},
7979

8080
[`& .${classes.notchedChild}`]: {
81-
padding: theme.spacing(1),
81+
padding: theme.spacing(1, 1.75),
8282
},
8383
[`& .${classes.verticalHr}`]: {
8484
display: "flex",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from "react"
2+
import { render, screen, fireEvent, within } from "@testing-library/react"
3+
import "@testing-library/jest-dom"
4+
import ValidateEvent, { ValidateEventProps } from "."
5+
import { EventCtx, EventPayload } from ".."
6+
import useValidateEvent from "./useValidateEvent"
7+
import { EventType } from "../types"
8+
9+
// Mock the useValidateEvent hook. This allows us to control its output and check if it's called correctly.
10+
jest.mock("./useValidateEvent", () => ({
11+
__esModule: true,
12+
default: jest.fn(),
13+
}))
14+
15+
const mockedUseValidateEvent = useValidateEvent as jest.Mock
16+
17+
// Mock child components that are not relevant to this test.
18+
jest.mock("@/components/PrettyJson", () => () => <div>PrettyJson</div>)
19+
jest.mock("@/components/Spinner", () => () => <div>Spinner</div>)
20+
21+
const mockValidateEventFn = jest.fn()
22+
23+
// A minimal set of props to render the component.
24+
const defaultProps: ValidateEventProps = {
25+
measurement_id: "",
26+
app_instance_id: "",
27+
firebase_app_id: "",
28+
api_secret: "",
29+
client_id: "",
30+
user_id: "",
31+
formatPayload: jest.fn(),
32+
payloadErrors: undefined,
33+
useTextBox: false,
34+
}
35+
36+
// A helper to render the component with context.
37+
const renderComponent = (props: Partial<ValidateEventProps> = {}) => {
38+
// The component relies on EventCtx for some data. This should be a valid
39+
// EventPayload.
40+
const contextValue: EventPayload = {
41+
instanceId: {
42+
measurement_id: "G-12345",
43+
firebase_app_id: "app:12345",
44+
},
45+
eventName: "test_event",
46+
type: EventType.CustomEvent,
47+
parameters: [],
48+
items: [],
49+
userProperties: [],
50+
timestamp_micros: "",
51+
non_personalized_ads: false,
52+
useTextBox: false,
53+
payloadObj: [],
54+
api_secret: "secret123",
55+
clientIds: {},
56+
}
57+
58+
return render(
59+
<EventCtx.Provider value={contextValue}>
60+
<ValidateEvent {...defaultProps} {...props} />
61+
</EventCtx.Provider>
62+
)
63+
}
64+
65+
describe("ValidateEvent EU endpoint functionality", () => {
66+
beforeEach(() => {
67+
// Reset mocks before each test
68+
jest.clearAllMocks()
69+
// Setup the default mock implementation for useValidateEvent to render the initial state.
70+
mockedUseValidateEvent.mockReturnValue({
71+
status: "not-started",
72+
validateEvent: mockValidateEventFn,
73+
})
74+
})
75+
76+
it("should render with the default endpoint and allow switching to the EU endpoint", () => {
77+
renderComponent()
78+
79+
// 1. Check initial state (default endpoint)
80+
expect(screen.getByText("HOST: www.google-analytics.com", { exact: false })).toBeInTheDocument()
81+
expect(screen.queryByText("HOST: region1.google-analytics.com", { exact: false })).not.toBeInTheDocument()
82+
expect(mockedUseValidateEvent).toHaveBeenCalledWith(false)
83+
84+
// 2. Find and interact with the switch
85+
const euSwitch = within(screen.getByTestId("use-eu-endpoint")).getByRole('checkbox')
86+
expect(euSwitch).toHaveProperty('checked', false)
87+
fireEvent.click(euSwitch)
88+
89+
// 3. Check the new state (EU endpoint)
90+
expect(euSwitch).toHaveProperty('checked', true)
91+
expect(screen.getByText("HOST: region1.google-analytics.com", { exact: false })).toBeInTheDocument()
92+
expect(mockedUseValidateEvent).toHaveBeenCalledTimes(2)
93+
expect(mockedUseValidateEvent).toHaveBeenLastCalledWith(true)
94+
})
95+
})

src/components/ga4/EventBuilder/ValidateEvent/index.tsx

Lines changed: 110 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ import clsx from "classnames"
2121
import useValidateEvent from "./useValidateEvent"
2222
import Loadable from "@/components/Loadable"
2323
import Typography from "@mui/material/Typography"
24+
import Grid from "@mui/material/Grid"
25+
import Switch from "@mui/material/Switch"
2426
import { PAB, PlainButton } from "@/components/Buttons"
2527
import { Check, Warning, Error as ErrorIcon } from "@mui/icons-material"
2628
import PrettyJson from "@/components/PrettyJson"
2729
import usePayload from "./usePayload"
2830
import { ValidationMessage } from "../types"
2931
import Spinner from "@/components/Spinner"
3032
import { EventCtx, Label } from ".."
31-
import { Card } from "@mui/material"
33+
import { Box, Card } from "@mui/material"
3234
import { green, red } from "@mui/material/colors"
35+
import WithHelpText from "@/components/WithHelpText"
3336

3437
const PREFIX = 'ValidateEvent';
3538

@@ -47,6 +50,7 @@ interface TemplateProps {
4750
sent?: boolean
4851
payloadErrors?: string | undefined
4952
useTextBox?: boolean
53+
useEuEndpoint: boolean
5054
}
5155

5256
export interface ValidateEventProps {
@@ -166,16 +170,14 @@ const Template: React.FC<TemplateProps> = ({
166170
error,
167171
valid,
168172
payloadErrors,
169-
useTextBox
173+
useTextBox,
174+
useEuEndpoint,
170175
}) => {
171176

172177
const { instanceId, api_secret } = useContext(EventCtx)!
173178
const payload = usePayload()
174179
return (
175-
<Card
176-
className={clsx(classes.form, classes.template)}
177-
data-testid="validate and send"
178-
>
180+
<>
179181
<Typography className={classes.heading} variant="h3">
180182
{headingIcon}
181183
{heading}
@@ -249,9 +251,9 @@ const Template: React.FC<TemplateProps> = ({
249251
{instanceId.firebase_app_id &&
250252
`&firebase_app_id=${instanceId.firebase_app_id}`}
251253
{instanceId.measurement_id &&
252-
`&measurement_id=${instanceId.measurement_id}`}{" "}
254+
`&measurement_id=${instanceId.measurement_id}`}{" "} <br />
253255
HTTP/1.1 <br />
254-
HOST: www.google-analytics.com <br />
256+
HOST: {useEuEndpoint ? "region1.google-analytics.com" : "www.google-analytics.com"} <br />
255257
Content-Type: application/json
256258
</Typography>
257259

@@ -265,87 +267,124 @@ const Template: React.FC<TemplateProps> = ({
265267
tooltipText="Copy payload"
266268
/>
267269
</section>
268-
</Card>
270+
</>
269271
)
270272
}
271273

272274
const ValidateEvent: React.FC<ValidateEventProps> = ({formatPayload, payloadErrors, useTextBox}) => {
273-
const request = useValidateEvent()
275+
const [useEuEndpoint, setUseEuEndpoint] = React.useState(false)
276+
const request = useValidateEvent(useEuEndpoint)
274277

275278
return (
276-
<Loadable
277-
request={request}
278-
renderNotStarted={({ validateEvent }) => (
279-
<Template
280-
heading="This event has not been validated"
281-
headingIcon={<Warning />}
282-
body={
283-
<Root>
284-
<Typography>
285-
Update the event using the controls above.
286-
</Typography>
287-
<Typography>
288-
When you're done editing the event, click "Validate Event" to
289-
check if the event is valid.
290-
</Typography>
291-
</Root>
292-
}
293-
validateEvent={ () => {
279+
<Root className={classes.form}>
280+
<Box mb={1} className={clsx(classes.form, classes.template)}>
281+
<WithHelpText
282+
notched
283+
shrink
284+
label="server endpoint"
285+
helpText="The default endpoint is https://www.google-analytics.com. If 'EU' is selected, the https://region1.google-analytics.com endpoint will be used to validate and send events."
286+
>
287+
<Grid component="label" container alignItems="center" spacing={1}>
288+
<Grid item>Default</Grid>
289+
<Grid item>
290+
<Switch
291+
data-testid="use-eu-endpoint"
292+
checked={useEuEndpoint}
293+
onChange={e => setUseEuEndpoint(e.target.checked)}
294+
name="use-eu-endpoint"
295+
color="primary"
296+
/>
297+
</Grid>
298+
<Grid item>EU</Grid>
299+
</Grid>
300+
</WithHelpText>
301+
</Box>
302+
<Card className={clsx(classes.form, classes.template)} data-testid="validate and send">
303+
<Loadable
304+
request={request}
305+
renderNotStarted={({ validateEvent }) => (
306+
<Template
307+
useEuEndpoint={useEuEndpoint}
308+
heading="This event has not been validated"
309+
headingIcon={<Warning />}
310+
body={
311+
<>
312+
<Typography>
313+
Update the event using the controls above.
314+
</Typography>
315+
<Typography>
316+
When you're done editing the event, click "Validate Event" to
317+
check if the event is valid.
318+
</Typography>
319+
</>
320+
}
321+
validateEvent={() => {
294322
if (formatPayload) {
295323
formatPayload()
296324
}
297325

298326
validateEvent()
299-
}
300-
}
301-
/>
302-
)}
303-
renderInProgress={() => (
304-
<Template heading="Validating" body={<Spinner ellipses />} />
305-
)}
306-
renderFailed={({ validationMessages, validateEvent}) => (
307-
<Template
308-
error
309-
headingIcon={<ErrorIcon />}
310-
heading="Event is invalid"
311-
body=""
312-
validateEvent={ () => {
327+
}}
328+
/>
329+
)}
330+
renderInProgress={() => (
331+
<Template
332+
useEuEndpoint={useEuEndpoint}
333+
heading="Validating"
334+
body={<Spinner ellipses />}
335+
/>
336+
)}
337+
renderFailed={({ validationMessages, validateEvent }) => (
338+
<Template
339+
useEuEndpoint={useEuEndpoint}
340+
error
341+
headingIcon={<ErrorIcon />}
342+
heading="Event is invalid"
343+
body=""
344+
validateEvent={() => {
313345
if (formatPayload) {
314346
formatPayload()
315347
}
316348

317349
validateEvent()
350+
}}
351+
validationMessages={validationMessages}
352+
payloadErrors={payloadErrors}
353+
useTextBox={useTextBox}
354+
/>
355+
)}
356+
renderSuccessful={({
357+
sendToGA,
358+
copyPayload,
359+
copySharableLink,
360+
sent,
361+
}) => (
362+
<Template
363+
useEuEndpoint={useEuEndpoint}
364+
sent={sent}
365+
valid
366+
heading="Event is valid"
367+
headingIcon={<Check />}
368+
sendToGA={sendToGA}
369+
copyPayload={copyPayload}
370+
copySharableLink={copySharableLink}
371+
body={
372+
<>
373+
<Typography>
374+
Use the controls below to copy the event payload or share it
375+
with coworkers.
376+
</Typography>
377+
<Typography>
378+
You can also send the event to Google Analytics and watch it in
379+
action in the Real Time view.
380+
</Typography>
381+
</>
318382
}
319-
}
320-
validationMessages={validationMessages}
321-
payloadErrors={payloadErrors}
322-
useTextBox={useTextBox}
323-
/>
324-
)}
325-
renderSuccessful={({ sendToGA, copyPayload, copySharableLink, sent}) => (
326-
<Template
327-
sent={sent}
328-
valid
329-
heading="Event is valid"
330-
headingIcon={<Check />}
331-
sendToGA={sendToGA}
332-
copyPayload={copyPayload}
333-
copySharableLink={copySharableLink}
334-
body={
335-
<>
336-
<Typography>
337-
Use the controls below to copy the event payload or share it
338-
with coworkers.
339-
</Typography>
340-
<Typography>
341-
You can also send the event to Google Analytics and watch it in
342-
action in the Real Time view.
343-
</Typography>
344-
</>
345-
}
383+
/>
384+
)}
346385
/>
347-
)}
348-
/>
386+
</Card>
387+
</Root>
349388
);
350389
}
351390

0 commit comments

Comments
 (0)