diff --git a/src/components/WithHelpText.tsx b/src/components/WithHelpText.tsx
index a99dd6e64..b4cb170f0 100644
--- a/src/components/WithHelpText.tsx
+++ b/src/components/WithHelpText.tsx
@@ -78,7 +78,7 @@ const Root = styled('div')((
},
[`& .${classes.notchedChild}`]: {
- padding: theme.spacing(1),
+ padding: theme.spacing(1, 1.75),
},
[`& .${classes.verticalHr}`]: {
display: "flex",
diff --git a/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx b/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx
new file mode 100644
index 000000000..0ed90e1a2
--- /dev/null
+++ b/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx
@@ -0,0 +1,95 @@
+import React from "react"
+import { render, screen, fireEvent, within } from "@testing-library/react"
+import "@testing-library/jest-dom"
+import ValidateEvent, { ValidateEventProps } from "."
+import { EventCtx, EventPayload } from ".."
+import useValidateEvent from "./useValidateEvent"
+import { EventType } from "../types"
+
+// Mock the useValidateEvent hook. This allows us to control its output and check if it's called correctly.
+jest.mock("./useValidateEvent", () => ({
+ __esModule: true,
+ default: jest.fn(),
+}))
+
+const mockedUseValidateEvent = useValidateEvent as jest.Mock
+
+// Mock child components that are not relevant to this test.
+jest.mock("@/components/PrettyJson", () => () =>
PrettyJson
)
+jest.mock("@/components/Spinner", () => () => Spinner
)
+
+const mockValidateEventFn = jest.fn()
+
+// A minimal set of props to render the component.
+const defaultProps: ValidateEventProps = {
+ measurement_id: "",
+ app_instance_id: "",
+ firebase_app_id: "",
+ api_secret: "",
+ client_id: "",
+ user_id: "",
+ formatPayload: jest.fn(),
+ payloadErrors: undefined,
+ useTextBox: false,
+}
+
+// A helper to render the component with context.
+const renderComponent = (props: Partial = {}) => {
+ // The component relies on EventCtx for some data. This should be a valid
+ // EventPayload.
+ const contextValue: EventPayload = {
+ instanceId: {
+ measurement_id: "G-12345",
+ firebase_app_id: "app:12345",
+ },
+ eventName: "test_event",
+ type: EventType.CustomEvent,
+ parameters: [],
+ items: [],
+ userProperties: [],
+ timestamp_micros: "",
+ non_personalized_ads: false,
+ useTextBox: false,
+ payloadObj: [],
+ api_secret: "secret123",
+ clientIds: {},
+ }
+
+ return render(
+
+
+
+ )
+}
+
+describe("ValidateEvent EU endpoint functionality", () => {
+ beforeEach(() => {
+ // Reset mocks before each test
+ jest.clearAllMocks()
+ // Setup the default mock implementation for useValidateEvent to render the initial state.
+ mockedUseValidateEvent.mockReturnValue({
+ status: "not-started",
+ validateEvent: mockValidateEventFn,
+ })
+ })
+
+ it("should render with the default endpoint and allow switching to the EU endpoint", () => {
+ renderComponent()
+
+ // 1. Check initial state (default endpoint)
+ expect(screen.getByText("HOST: www.google-analytics.com", { exact: false })).toBeInTheDocument()
+ expect(screen.queryByText("HOST: region1.google-analytics.com", { exact: false })).not.toBeInTheDocument()
+ expect(mockedUseValidateEvent).toHaveBeenCalledWith(false)
+
+ // 2. Find and interact with the switch
+ const euSwitch = within(screen.getByTestId("use-eu-endpoint")).getByRole('checkbox')
+ expect(euSwitch).toHaveProperty('checked', false)
+ fireEvent.click(euSwitch)
+
+ // 3. Check the new state (EU endpoint)
+ expect(euSwitch).toHaveProperty('checked', true)
+ expect(screen.getByText("HOST: region1.google-analytics.com", { exact: false })).toBeInTheDocument()
+ expect(mockedUseValidateEvent).toHaveBeenCalledTimes(2)
+ expect(mockedUseValidateEvent).toHaveBeenLastCalledWith(true)
+ })
+})
diff --git a/src/components/ga4/EventBuilder/ValidateEvent/index.tsx b/src/components/ga4/EventBuilder/ValidateEvent/index.tsx
index 43fa96e48..4bff53753 100644
--- a/src/components/ga4/EventBuilder/ValidateEvent/index.tsx
+++ b/src/components/ga4/EventBuilder/ValidateEvent/index.tsx
@@ -21,6 +21,8 @@ import clsx from "classnames"
import useValidateEvent from "./useValidateEvent"
import Loadable from "@/components/Loadable"
import Typography from "@mui/material/Typography"
+import Grid from "@mui/material/Grid"
+import Switch from "@mui/material/Switch"
import { PAB, PlainButton } from "@/components/Buttons"
import { Check, Warning, Error as ErrorIcon } from "@mui/icons-material"
import PrettyJson from "@/components/PrettyJson"
@@ -28,8 +30,9 @@ import usePayload from "./usePayload"
import { ValidationMessage } from "../types"
import Spinner from "@/components/Spinner"
import { EventCtx, Label } from ".."
-import { Card } from "@mui/material"
+import { Box, Card } from "@mui/material"
import { green, red } from "@mui/material/colors"
+import WithHelpText from "@/components/WithHelpText"
const PREFIX = 'ValidateEvent';
@@ -47,6 +50,7 @@ interface TemplateProps {
sent?: boolean
payloadErrors?: string | undefined
useTextBox?: boolean
+ useEuEndpoint: boolean
}
export interface ValidateEventProps {
@@ -166,16 +170,14 @@ const Template: React.FC = ({
error,
valid,
payloadErrors,
- useTextBox
+ useTextBox,
+ useEuEndpoint,
}) => {
const { instanceId, api_secret } = useContext(EventCtx)!
const payload = usePayload()
return (
-
+ <>
{headingIcon}
{heading}
@@ -249,9 +251,9 @@ const Template: React.FC = ({
{instanceId.firebase_app_id &&
`&firebase_app_id=${instanceId.firebase_app_id}`}
{instanceId.measurement_id &&
- `&measurement_id=${instanceId.measurement_id}`}{" "}
+ `&measurement_id=${instanceId.measurement_id}`}{" "}
HTTP/1.1
- HOST: www.google-analytics.com
+ HOST: {useEuEndpoint ? "region1.google-analytics.com" : "www.google-analytics.com"}
Content-Type: application/json
@@ -265,87 +267,124 @@ const Template: React.FC = ({
tooltipText="Copy payload"
/>
-
+ >
)
}
const ValidateEvent: React.FC = ({formatPayload, payloadErrors, useTextBox}) => {
- const request = useValidateEvent()
+ const [useEuEndpoint, setUseEuEndpoint] = React.useState(false)
+ const request = useValidateEvent(useEuEndpoint)
return (
- (
- }
- body={
-
-
- Update the event using the controls above.
-
-
- When you're done editing the event, click "Validate Event" to
- check if the event is valid.
-
-
- }
- validateEvent={ () => {
+
+
+
+
+ Default
+
+ setUseEuEndpoint(e.target.checked)}
+ name="use-eu-endpoint"
+ color="primary"
+ />
+
+ EU
+
+
+
+
+ (
+ }
+ body={
+ <>
+
+ Update the event using the controls above.
+
+
+ When you're done editing the event, click "Validate Event" to
+ check if the event is valid.
+
+ >
+ }
+ validateEvent={() => {
if (formatPayload) {
formatPayload()
}
validateEvent()
- }
- }
- />
- )}
- renderInProgress={() => (
- } />
- )}
- renderFailed={({ validationMessages, validateEvent}) => (
- }
- heading="Event is invalid"
- body=""
- validateEvent={ () => {
+ }}
+ />
+ )}
+ renderInProgress={() => (
+ }
+ />
+ )}
+ renderFailed={({ validationMessages, validateEvent }) => (
+ }
+ heading="Event is invalid"
+ body=""
+ validateEvent={() => {
if (formatPayload) {
formatPayload()
}
validateEvent()
+ }}
+ validationMessages={validationMessages}
+ payloadErrors={payloadErrors}
+ useTextBox={useTextBox}
+ />
+ )}
+ renderSuccessful={({
+ sendToGA,
+ copyPayload,
+ copySharableLink,
+ sent,
+ }) => (
+ }
+ sendToGA={sendToGA}
+ copyPayload={copyPayload}
+ copySharableLink={copySharableLink}
+ body={
+ <>
+
+ Use the controls below to copy the event payload or share it
+ with coworkers.
+
+
+ You can also send the event to Google Analytics and watch it in
+ action in the Real Time view.
+
+ >
}
- }
- validationMessages={validationMessages}
- payloadErrors={payloadErrors}
- useTextBox={useTextBox}
- />
- )}
- renderSuccessful={({ sendToGA, copyPayload, copySharableLink, sent}) => (
- }
- sendToGA={sendToGA}
- copyPayload={copyPayload}
- copySharableLink={copySharableLink}
- body={
- <>
-
- Use the controls below to copy the event payload or share it
- with coworkers.
-
-
- You can also send the event to Google Analytics and watch it in
- action in the Real Time view.
-
- >
- }
+ />
+ )}
/>
- )}
- />
+
+
);
}
diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts b/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts
index 926743918..9fb56ca14 100644
--- a/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts
+++ b/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts
@@ -31,14 +31,20 @@ const instanceQueryParamFor = (instanceId: InstanceId) => {
return ``
}
+const getEndpoint = (useEuEndpoint: boolean) => {
+ if (useEuEndpoint) {
+ return "https://region1.google-analytics.com"
+ }
+ return "https://www.google-analytics.com"
+}
+
const validateHit = async (
payload: {},
instanceId: InstanceId,
- api_secret: string
+ api_secret: string,
+ baseUrl: string
): Promise => {
- const url = `https://www.google-analytics.com/debug/mp/collect?api_secret=${api_secret}${instanceQueryParamFor(
- instanceId
- )}`
+ const url = `${baseUrl}/debug/mp/collect?api_secret=${api_secret}${instanceQueryParamFor(instanceId)}`
const body = Object.assign({}, payload, {
validationBehavior: "ENFORCE_RECOMMENDATIONS",
})
@@ -53,11 +59,10 @@ const validateHit = async (
const sendHit = async (
payload: {},
instanceId: InstanceId,
- api_secret: string
+ api_secret: string,
+ baseUrl: string
): Promise => {
- const url = `https://www.google-analytics.com/mp/collect?api_secret=${api_secret}${instanceQueryParamFor(
- instanceId
- )}`
+ const url = `${baseUrl}/mp/collect?api_secret=${api_secret}${instanceQueryParamFor(instanceId)}`
const body = Object.assign({}, payload, {
validationBehavior: "ENFORCE_RECOMMENDATIONS",
})
@@ -86,7 +91,7 @@ export const ValidationRequestCtx = createContext<
ReturnType | undefined
>(undefined)
-const useValidateEvent = (): Requestable<
+const useValidateEvent = (useEuEndpoint: boolean): Requestable<
ValidationSuccessful,
ValidationNotStarted,
ValidationInProgress,
@@ -116,8 +121,8 @@ const useValidateEvent = (): Requestable<
if (status !== RequestStatus.Successful) {
return
}
- sendHit(payload, instanceId, api_secret).then(() => setSent(true))
- }, [status, payload, instanceId, api_secret])
+ sendHit(payload, instanceId, api_secret, getEndpoint(useEuEndpoint)).then(() => setSent(true))
+ }, [status, payload, instanceId, api_secret, useEuEndpoint])
const copyPayload = useCopy(
JSON.stringify(payload, undefined, " "),
@@ -162,7 +167,7 @@ const useValidateEvent = (): Requestable<
if (!useTextBox || Object.keys(payload).length !== 0) {
let validatorErrors = validatePayloadAttributes(payload)
- validateHit(payload, instanceId, api_secret)
+ validateHit(payload, instanceId, api_secret, getEndpoint(useEuEndpoint))
.then(messages => {
setTimeout(() => {
if (messages.length > 0 || validatorErrors.length > 0) {
@@ -197,7 +202,7 @@ const useValidateEvent = (): Requestable<
setValidationMessages(validatorErrors)
setStatus(RequestStatus.Failed)
}
- }, [status, payload, api_secret, instanceId, useFirebase, useTextBox, validatePayloadAttributes])
+ }, [status, payload, api_secret, instanceId, useFirebase, useTextBox, validatePayloadAttributes, useEuEndpoint])
const defineFieldCode = (error: JSONError) => {
const { data } = error
diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx
index 34fed9eaa..dfe3a04f4 100644
--- a/src/components/ga4/EventBuilder/index.tsx
+++ b/src/components/ga4/EventBuilder/index.tsx
@@ -160,7 +160,6 @@ export const ShowAdvancedCtx = React.createContext(false)
export const UseFirebaseCtx = React.createContext(false)
const EventBuilder: React.FC = () => {
-
const [showAdvanced, setShowAdvanced] = React.useState(false)
const {
userProperties,