From 93c325a803ff2585b4b9b205e03bccf78353d7de Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Wed, 27 Aug 2025 14:36:13 +0000 Subject: [PATCH 01/23] allow screen_view and ad_impression events --- .../EventBuilder/ValidateEvent/handlers/formatCheckLib.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts index b6a72d53..1fade516 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts @@ -5,12 +5,11 @@ import {eventDefinitions} from "../schemas/eventTypes/eventDefinitions" import {InstanceId} from "../../types" const RESERVED_EVENT_NAMES = [ - "ad_activeview", "ad_click", "ad_exposure", "ad_impression", "ad_query", + "ad_activeview", "ad_click", "ad_exposure", "ad_query", "adunit_exposure", "app_clear_data", "app_install", "app_update", "app_remove", "error", "first_open", "first_visit", "in_app_purchase", "notification_dismiss", "notification_foreground", "notification_open", - "notification_receive", "os_update", "screen_view", "session_start", - "user_engagement" + "notification_receive", "os_update", "session_start", "user_engagement" ] const RESERVED_USER_PROPERTY_NAMES = [ "first_open_time", "first_visit_time", "last_deep_link_referrer", "user_id", From a79e34a323d0214564b8b026dd6f1560cdecabd5 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Wed, 27 Aug 2025 17:52:47 +0000 Subject: [PATCH 02/23] allow geo information fields --- .../ValidateEvent/schemas/baseContent.spec.ts | 12 +++ .../ValidateEvent/schemas/baseContent.ts | 51 ++++++----- .../schemas/userLocation.spec.ts | 84 +++++++++++++++++++ .../ValidateEvent/schemas/userLocation.ts | 31 +++++++ 4 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts create mode 100644 src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts index d34705f8..68e5b7e2 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts @@ -142,4 +142,16 @@ describe("baseContentSchema", () => { expect(validator.isValid(validInput)).toEqual(false) }) + + describe("with ip_override", () => { + test("is valid with a valid IPv4 address", () => { + const validInput = { + events: [{ name: "something", params: {} }], + ip_override: "127.0.0.1", + } + const validator = new Validator(baseContentSchema) + expect(validator.isValid(validInput)).toEqual(true) + }) + }) + }) \ No newline at end of file diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts index 303834fb..0245e26b 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts @@ -2,29 +2,34 @@ import { userPropertiesSchema } from './userProperties' import { eventsSchema } from './events' +import { userLocationSchema } from "./userLocation" export const baseContentSchema = { - "type": "object", - "required": ["events"], - "additionalProperties": false, - "properties": { - "app_instance_id": { - "type": "string", - "format": "app_instance_id" - }, - "client_id": { - "type": "string", - }, - "user_id": { - "type": "string" - }, - "timestamp_micros": { - // "type": "number" - }, - "user_properties": userPropertiesSchema, - "non_personalized_ads": { - "type": "boolean" - }, - "events": eventsSchema, - } + type: "object", + required: ["events"], + additionalProperties: false, + properties: { + app_instance_id: { + type: "string", + format: "app_instance_id", + }, + client_id: { + type: "string", + }, + user_id: { + type: "string", + }, + timestamp_micros: { + // "type": "number" + }, + user_properties: userPropertiesSchema, + non_personalized_ads: { + type: "boolean", + }, + events: eventsSchema, + user_location: userLocationSchema, + ip_override: { + type: "string", + }, + }, } \ No newline at end of file diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts new file mode 100644 index 00000000..0ae653ea --- /dev/null +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts @@ -0,0 +1,84 @@ +import "jest" +import { Validator } from "../validator" +import { userLocationSchema } from "./userLocation" + +describe("userLocationSchema", () => { + test("can be used to validate a valid payload", () => { + const validInput = { + city: "Mountain View", + region_id: "US-CA", + country_id: "US", + subcontinent_id: "021", + continent_id: "019", + } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(validInput)).toEqual(true) + }) + + test.each([["US-CA"], ["US-C"], ["US-C1A"]])( + "is valid with a valid region_id: %s", + region_id => { + const validator = new Validator(userLocationSchema) + expect(validator.isValid({ region_id })).toEqual(true) + } + ) + + test.each([["USA-CA"], ["US-CALI"], ["us-ca"], ["US_CA"]])( + "is invalid with an invalid region_id: %s", + region_id => { + const validator = new Validator(userLocationSchema) + expect(validator.isValid({ region_id })).toEqual(false) + } + ) + + test("is valid with a valid country_id", () => { + const validInput = { country_id: "US" } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(validInput)).toEqual(true) + }) + + test.each([["USA"], ["U"], ["us"]])( + "is invalid with an invalid country_id: %s", + country_id => { + const validator = new Validator(userLocationSchema) + expect(validator.isValid({ country_id })).toEqual(false) + } + ) + + test("is valid with a valid subcontinent_id", () => { + const validInput = { subcontinent_id: "021" } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(validInput)).toEqual(true) + }) + + test.each([["21"], ["0211"], ["abc"]])( + "is invalid with an invalid subcontinent_id: %s", + subcontinent_id => { + const validator = new Validator(userLocationSchema) + expect(validator.isValid({ subcontinent_id })).toEqual(false) + } + ) + + test("is valid with a valid continent_id", () => { + const validInput = { continent_id: "019" } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(validInput)).toEqual(true) + }) + + test.each([["19"], ["0199"], ["abc"]])( + "is invalid with an invalid continent_id: %s", + continent_id => { + const validator = new Validator(userLocationSchema) + expect(validator.isValid({ continent_id })).toEqual(false) + } + ) + + test("is invalid with additional properties", () => { + const invalidInput = { + city: "Mountain View", + extra_prop: "should fail", + } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(invalidInput)).toEqual(false) + }) +}) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts new file mode 100644 index 00000000..271b4b73 --- /dev/null +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts @@ -0,0 +1,31 @@ +// User Location Schema + +export const userLocationSchema = { + type: "object", + additionalProperties: false, + properties: { + city: { + type: "string", + }, + region_id: { + type: "string", + // ISO 3166-2 + pattern: "^[A-Z]{2}-[A-Z0-9]{1,3}$", + }, + country_id: { + type: "string", + // ISO 3166-1 alpha-2 + pattern: "^[A-Z]{2}$", + }, + subcontinent_id: { + type: "string", + // UN M49 + pattern: "^[0-9]{3}$", + }, + continent_id: { + type: "string", + // UN M49 + pattern: "^[0-9]{3}$", + }, + }, +} From ba03bc30eedfb571081d8b87a4556d273df32ca7 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 28 Aug 2025 18:17:39 +0000 Subject: [PATCH 03/23] add support for geo fields --- .../EventBuilder/GeographicInformation.tsx | 155 +++++ .../ga4/EventBuilder/ValidateEvent/index.tsx | 3 +- .../schemas/userLocation.spec.ts | 48 +- .../ValidateEvent/schemas/userLocation.ts | 8 - .../EventBuilder/ValidateEvent/usePayload.ts | 26 +- .../ValidateEvent/useSharableLink.ts | 17 + .../ga4/EventBuilder/index.spec.tsx | 3 +- src/components/ga4/EventBuilder/index.tsx | 630 +++++++++--------- src/components/ga4/EventBuilder/types.ts | 89 ++- src/components/ga4/EventBuilder/useInputs.ts | 46 +- src/constants.ts | 7 + 11 files changed, 664 insertions(+), 368 deletions(-) create mode 100644 src/components/ga4/EventBuilder/GeographicInformation.tsx diff --git a/src/components/ga4/EventBuilder/GeographicInformation.tsx b/src/components/ga4/EventBuilder/GeographicInformation.tsx new file mode 100644 index 00000000..11b621da --- /dev/null +++ b/src/components/ga4/EventBuilder/GeographicInformation.tsx @@ -0,0 +1,155 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from "react" +import Chip from "@mui/material/Chip"; +import Divider from '@mui/material/Divider'; +import { styled } from "@mui/material/styles" +import Typography from "@mui/material/Typography" +import TextField from "@mui/material/TextField" +import Grid from "@mui/material/Grid" + +import LinkedTextField from "@/components/LinkedTextField" +import ExternalLink from "@/components/ExternalLink" +import { Label } from "./types" + +const Root = styled("div")(({ theme }) => ({ + marginTop: theme.spacing(3), +})) + + +interface GeographicInformationProps { + user_location_city: string | undefined + setUserLocationCity: (value: string) => void + user_location_region_id: string | undefined + setUserLocationRegionId: (value: string) => void + user_location_country_id: string | undefined + setUserLocationCountryId: (value: string) => void + user_location_subcontinent_id: string | undefined + setUserLocationSubcontinentId: (value: string) => void + user_location_continent_id: string | undefined + setUserLocationContinentId: (value: string) => void + ip_override: string | undefined + setIpOverride: (value: string) => void +} + +const GeographicInformation: React.FC = ({ + user_location_city, + setUserLocationCity, + user_location_region_id, + setUserLocationRegionId, + user_location_country_id, + setUserLocationCountryId, + user_location_subcontinent_id, + setUserLocationSubcontinentId, + user_location_continent_id, + setUserLocationContinentId, + ip_override, + setIpOverride, +}) => { + return ( + + {/* Geographic Information */} + + User Location + + See the{" "} + + documentation + {" "} + for more information about user location attributes. + + + + setUserLocationCity(e.target.value)} + helperText="The city of the user. E.g. 'Mountain View'." + /> + + + setUserLocationRegionId(e.target.value)} + helperText="The region of the user. E.g. 'US-CA'." + /> + + + setUserLocationCountryId(e.target.value)} + helperText="The country of the user. E.g. 'US'." + /> + + + setUserLocationSubcontinentId(e.target.value)} + helperText="The subcontinent of the user. E.g. '021'." + /> + + + setUserLocationContinentId(e.target.value)} + helperText="The continent of the user. E.g. '019'." + /> + + + IP Override + + Specify an IP address as an alternative to sending user location. This + IP address will be used to derive the user's geographic information. If + both an IP override and user location are provided, user location will + be used. + + + + ) +} + +export default GeographicInformation \ No newline at end of file diff --git a/src/components/ga4/EventBuilder/ValidateEvent/index.tsx b/src/components/ga4/EventBuilder/ValidateEvent/index.tsx index 43fa96e4..7806f3fd 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/index.tsx +++ b/src/components/ga4/EventBuilder/ValidateEvent/index.tsx @@ -27,7 +27,8 @@ import PrettyJson from "@/components/PrettyJson" import usePayload from "./usePayload" import { ValidationMessage } from "../types" import Spinner from "@/components/Spinner" -import { EventCtx, Label } from ".." +import { EventCtx } from ".." +import { Label } from "../types" import { Card } from "@mui/material" import { green, red } from "@mui/material/colors" diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts index 0ae653ea..5384f3fa 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts @@ -23,13 +23,11 @@ describe("userLocationSchema", () => { } ) - test.each([["USA-CA"], ["US-CALI"], ["us-ca"], ["US_CA"]])( - "is invalid with an invalid region_id: %s", - region_id => { - const validator = new Validator(userLocationSchema) - expect(validator.isValid({ region_id })).toEqual(false) - } - ) + test("is invalid with an invalid region_id", () => { + const invalidInput = { region_id: 123 } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(invalidInput)).toEqual(false) + }) test("is valid with a valid country_id", () => { const validInput = { country_id: "US" } @@ -37,13 +35,11 @@ describe("userLocationSchema", () => { expect(validator.isValid(validInput)).toEqual(true) }) - test.each([["USA"], ["U"], ["us"]])( - "is invalid with an invalid country_id: %s", - country_id => { - const validator = new Validator(userLocationSchema) - expect(validator.isValid({ country_id })).toEqual(false) - } - ) + test("is invalid with an invalid country_id", () => { + const invalidInput = { country_id: 1 } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(invalidInput)).toEqual(false) + }) test("is valid with a valid subcontinent_id", () => { const validInput = { subcontinent_id: "021" } @@ -51,13 +47,11 @@ describe("userLocationSchema", () => { expect(validator.isValid(validInput)).toEqual(true) }) - test.each([["21"], ["0211"], ["abc"]])( - "is invalid with an invalid subcontinent_id: %s", - subcontinent_id => { - const validator = new Validator(userLocationSchema) - expect(validator.isValid({ subcontinent_id })).toEqual(false) - } - ) + test("is invalid with an invalid subcontinent_id", () => { + const validInput = { subcontinent_id: 541 } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(validInput)).toEqual(false) + }) test("is valid with a valid continent_id", () => { const validInput = { continent_id: "019" } @@ -65,13 +59,11 @@ describe("userLocationSchema", () => { expect(validator.isValid(validInput)).toEqual(true) }) - test.each([["19"], ["0199"], ["abc"]])( - "is invalid with an invalid continent_id: %s", - continent_id => { - const validator = new Validator(userLocationSchema) - expect(validator.isValid({ continent_id })).toEqual(false) - } - ) + test("is invalid with an invalid continent_id", () => { + const validInput = { continent_id: 540 } + const validator = new Validator(userLocationSchema) + expect(validator.isValid(validInput)).toEqual(false) + }) test("is invalid with additional properties", () => { const invalidInput = { diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts index 271b4b73..312c15d6 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts @@ -9,23 +9,15 @@ export const userLocationSchema = { }, region_id: { type: "string", - // ISO 3166-2 - pattern: "^[A-Z]{2}-[A-Z0-9]{1,3}$", }, country_id: { type: "string", - // ISO 3166-1 alpha-2 - pattern: "^[A-Z]{2}$", }, subcontinent_id: { type: "string", - // UN M49 - pattern: "^[0-9]{3}$", }, continent_id: { type: "string", - // UN M49 - pattern: "^[0-9]{3}$", }, }, } diff --git a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts index 6c99e1f8..8b7423a2 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts @@ -71,7 +71,9 @@ const usePayload = (): {} => { clientIds, type, useTextBox, - payloadObj + payloadObj, + ip_override, + user_location, } = useContext(EventCtx)! const eventName = useMemo(() => { @@ -93,22 +95,34 @@ const usePayload = (): {} => { [items] ) - const params = useMemo(() => parameters.reduce(objectify, itemsParameter), [ - parameters, - itemsParameter, - ]) + const params = useMemo( + () => parameters.reduce(objectify, itemsParameter), + [parameters, itemsParameter] + ) const user_properties = useMemo( () => userProperties.reduce(objectifyUserProperties, {}), [userProperties] ) + const final_user_location = useMemo(() => { + if (user_location === undefined) { + return undefined + } + const cleaned_location = removeUndefined(user_location) + if (Object.keys(cleaned_location).length === 0) { + return undefined + } + return cleaned_location + }, [user_location]) + let payload = useMemo(() => { return { ...removeUndefined(clientIds), ...removeUndefined({ timestamp_micros }), ...removeUndefined({ non_personalized_ads }), ...removeUndefined(removeEmptyObject({ user_properties })), + ...removeUndefined({ ip_override, user_location: final_user_location }), events: [ { name: eventName, ...(parameters.length > 0 ? { params } : {}) }, ], @@ -121,6 +135,8 @@ const usePayload = (): {} => { params, timestamp_micros, user_properties, + ip_override, + final_user_location, ]) if (useTextBox) { diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts index dd2a63ab..feccefae 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts @@ -17,6 +17,8 @@ const useSharableLink = () => { parameters, eventName, type, + ip_override, + user_location, } = useContext(EventCtx)! return useMemo(() => { @@ -57,6 +59,19 @@ const useSharableLink = () => { addIfTruthy(UrlParam.TimestampMicros, timestamp_micros) + addIfTruthy(UrlParam.IpOverride, ip_override) + + if (user_location) { + addIfTruthy(UrlParam.UserLocationCity, user_location.city) + addIfTruthy(UrlParam.UserLocationRegionId, user_location.region_id) + addIfTruthy(UrlParam.UserLocationCountryId, user_location.country_id) + addIfTruthy( + UrlParam.UserLocationSubcontinentId, + user_location.subcontinent_id + ) + addIfTruthy(UrlParam.UserLocationContinentId, user_location.continent_id) + } + if (userProperties) { params.append(UrlParam.UserProperties, encodeObject(userProperties)) } @@ -84,6 +99,8 @@ const useSharableLink = () => { api_secret, timestamp_micros, non_personalized_ads, + ip_override, + user_location, ]) } diff --git a/src/components/ga4/EventBuilder/index.spec.tsx b/src/components/ga4/EventBuilder/index.spec.tsx index 16596010..b3f69833 100644 --- a/src/components/ga4/EventBuilder/index.spec.tsx +++ b/src/components/ga4/EventBuilder/index.spec.tsx @@ -18,7 +18,8 @@ import * as renderer from "@testing-library/react" import "@testing-library/jest-dom" import { withProviders } from "@/test-utils" -import Sut, { Label } from "./index" +import Sut from "./index" +import { Label } from "./types" import userEvent from "@testing-library/user-event" import { within } from "@testing-library/react" diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index aa185437..e293cc83 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -36,13 +36,14 @@ import { TooltipIconButton } from "@/components/Buttons" import useEvent from "./useEvent" import Parameters from "./Parameters" import useInputs from "./useInputs" -import { Category, ClientIds, EventType, InstanceId, Parameter } from "./types" +import { Category, ClientIds, EventType, InstanceId, Parameter, Label } from "./types" import { eventsForCategory } from "./event" import useUserProperties from "./useUserProperties" import Items from "./Items" import ValidateEvent from "./ValidateEvent" import { PlainButton } from "@/components/Buttons" import { useEffect } from "react" +import GeographicInformation from "./GeographicInformation"; const PREFIX = 'EventBuilder'; @@ -103,35 +104,6 @@ const Root = styled('div')(( } })); -export enum Label { - APISecret = "api secret", - - FirebaseAppID = "firebase app id", - AppInstanceID = "app instance id", - - MeasurementID = "measurement id", - ClientID = "client id", - - UserId = "user id", - - EventCategory = "event category", - EventName = "event name", - TimestampMicros = "timestamp micros", - NonPersonalizedAds = "non personalized ads", - - Payload = "payload", - - // event params - Coupon = '#/events/0/params/coupon', - Currency = '#/events/0/params/currency', - Value = '#/events/0/params/value', - ItemId = '#/events/0/params/item_id', - TransactionId = '#/events/0/params/transaction_id', - Affiliation = '#/events/0/params/affiliation', - Shipping = '#/events/0/params/shipping', - Tax = '#/events/0/params/tax', -} - const ga4MeasurementProtocol = ( GA4 Measurement Protocol @@ -151,16 +123,20 @@ export type EventPayload = { api_secret: string useTextBox: boolean payloadObj: any + ip_override: string | undefined + user_location: { + city: string | undefined + region_id: string | undefined + country_id: string | undefined + subcontinent_id: string | undefined + continent_id: string | undefined + } } -export const EventCtx = React.createContext< - | EventPayload - | undefined ->(undefined) +export const EventCtx = React.createContext(undefined) export const ShowAdvancedCtx = React.createContext(false) export const UseFirebaseCtx = React.createContext(false) const EventBuilder: React.FC = () => { - const [showAdvanced, setShowAdvanced] = React.useState(false) const { userProperties, @@ -224,20 +200,30 @@ const EventBuilder: React.FC = () => { setTimestampMicros, non_personalized_ads, setNonPersonalizedAds, + ip_override, + setIpOverride, + user_location_city, + setUserLocationCity, + user_location_region_id, + setUserLocationRegionId, + user_location_country_id, + setUserLocationCountryId, + user_location_subcontinent_id, + setUserLocationSubcontinentId, + user_location_continent_id, + setUserLocationContinentId, } = useInputs(categories) - const formatPayload = React.useCallback( () => { + const formatPayload = React.useCallback(() => { try { if (inputPayload) { let payload = JSON.parse(inputPayload) as object - setPayloadObj(JSON.stringify(payload, null, '\t')) - setPayloadErrors('') - } - else { - setPayloadErrors('Empty Payload') + setPayloadObj(JSON.stringify(payload, null, "\t")) + setPayloadErrors("") + } else { + setPayloadErrors("Empty Payload") setPayloadObj({}) } - } catch (err: any) { setPayloadErrors(err.message) setPayloadObj({}) @@ -248,10 +234,27 @@ const EventBuilder: React.FC = () => { formatPayload() }, [inputPayload, formatPayload]) + useEffect(() => { + // Clear geographic information on page load. + setIpOverride("") + setUserLocationCity("") + setUserLocationRegionId("") + setUserLocationCountryId("") + setUserLocationSubcontinentId("") + setUserLocationContinentId("") + }, [ + setIpOverride, + setUserLocationCity, + setUserLocationRegionId, + setUserLocationCountryId, + setUserLocationSubcontinentId, + setUserLocationContinentId, + ]) + return ( Overview - + The GA4 Event Builder allows you to create, validate, and send events using the {ga4MeasurementProtocol}. @@ -292,76 +295,75 @@ const EventBuilder: React.FC = () => { After choosing a client, fill out the inputs below. - - {useFirebase ? ( - <> - - - - ) : ( - <> - - - - )} - - { - + + {useFirebase ? ( + <> + + + + ) : ( + <> + + + + )} + + { <> { -
+
+ } - } - - { useTextBox && + {useTextBox && ( <> - 0 ? payloadObj : inputPayload} - label={Label.Payload} - onChange={(input) => { + 0 ? payloadObj : inputPayload + } + label={Label.Payload} + onChange={input => { setInputPayload(input) formatPayload() - } - } - /> + }} + /> -
+
-
+
- - format payload - + + format payload + - { payloadErrors && ( - - + {payloadErrors && ( + + - )} - - } + )} + + )} - { !useTextBox && -
-
- - - - data-testid={Label.EventCategory} - fullWidth - disableClearable - autoComplete - autoHighlight - autoSelect - options={Object.values(Category)} - getOptionLabel={category => category} - value={category} - onChange={(_event, value) => { - setCategory(value as Category) - const events = eventsForCategory(value as Category) - if (events.length > 0) { - setType(events[0].type) - } - }} - renderInput={params => ( - - )} - /> - {type === EventType.CustomEvent ? ( - { - setEventName(e.target.value) - }} - /> - ) : ( - - data-testid={Label.EventName} + {!useTextBox && ( +
+
+ + data-testid={Label.EventCategory} fullWidth disableClearable autoComplete autoHighlight autoSelect - options={eventsForCategory(category).map(e => e.type)} - getOptionLabel={eventType => eventType} - value={type} + options={Object.values(Category)} + getOptionLabel={category => category} + value={category} onChange={(_event, value) => { - setType(value as EventType) + setCategory(value as Category) + const events = eventsForCategory(value as Category) + if (events.length > 0) { + setType(events[0].type) + } }} renderInput={params => ( - The name of the event. - - } + helperText="The category for the event" /> )} /> - )} - - { - setTimestampMicros((new Date().getTime() * 1000).toString()) - }} - > - - - - } - /> + {type === EventType.CustomEvent ? ( + { + setEventName(e.target.value) + }} + /> + ) : ( + + data-testid={Label.EventName} + fullWidth + disableClearable + autoComplete + autoHighlight + autoSelect + options={eventsForCategory(category).map(e => e.type)} + getOptionLabel={eventType => eventType} + value={type} + onChange={(_event, value) => { + setType(value as EventType) + }} + renderInput={params => ( + The name of the event.} + /> + )} + /> + )} + + { + setTimestampMicros( + (new Date().getTime() * 1000).toString() + ) + }} + > + + + + } + /> - - Check to indicate events should not be used for personalized ads. - - } - > - + Check to indicate events should not be used for personalized + ads. + + } > - {Label.NonPersonalizedAds} - - -
- - Event details - - Finally, specify the parameters to send with the event. By default, only - recommended parameters for the event will appear here. Check "show - advanced options" to add custom parameters or user properties. - - - show advanced options - - -
- - Parameters - - {items !== undefined && ( - <> - Items - + {Label.NonPersonalizedAds} + + +
+ + Event details + + Finally, specify the parameters to send with the event. By default, + only recommended parameters for the event will appear here. Check + "show advanced options" to add custom parameters, user properties, + or geographic information. + + + show advanced options + + +
+ + Parameters + - - )} - {(showAdvanced || - (userProperties !== undefined && userProperties.length !== 0)) && ( - <> - User properties - - - )} - -
-
- } + {items !== undefined && ( + <> + Items + + + )} + {(showAdvanced || + (userProperties !== undefined && + userProperties.length !== 0)) && ( + <> + User properties + + + )} + {showAdvanced && ( + <> + + + )} + +
+
+ )} Validate & Send event @@ -624,6 +640,14 @@ const EventBuilder: React.FC = () => { payloadObj, instanceId: useFirebase ? { firebase_app_id } : { measurement_id }, api_secret: api_secret!, + ip_override, + user_location: { + city: user_location_city, + region_id: user_location_region_id, + country_id: user_location_country_id, + subcontinent_id: user_location_subcontinent_id, + continent_id: user_location_continent_id, + }, }} > {
- ); + ) } export default EventBuilder diff --git a/src/components/ga4/EventBuilder/types.ts b/src/components/ga4/EventBuilder/types.ts index f8d108ea..f9aeeb1b 100644 --- a/src/components/ga4/EventBuilder/types.ts +++ b/src/components/ga4/EventBuilder/types.ts @@ -84,28 +84,71 @@ export interface Event2 { items?: Parameter[][] } +export enum Label { + APISecret = "api secret", + + FirebaseAppID = "firebase app id", + AppInstanceID = "app instance id", + + MeasurementID = "measurement id", + ClientID = "client id", + + UserId = "user id", + + EventCategory = "event category", + EventName = "event name", + TimestampMicros = "timestamp micros", + NonPersonalizedAds = "non personalized ads", + + Payload = "payload", + + // event params + Coupon = "#/events/0/params/coupon", + Currency = "#/events/0/params/currency", + Value = "#/events/0/params/value", + ItemId = "#/events/0/params/item_id", + TransactionId = "#/events/0/params/transaction_id", + Affiliation = "#/events/0/params/affiliation", + Shipping = "#/events/0/params/shipping", + Tax = "#/events/0/params/tax", + + // Geographic Information + IpOverride = "ip address", + City = "city", + RegionId = "region id", + CountryId = "country id", + SubcontinentId = "subcontinent id", + ContinentId = "continent id", +} + // TODO - Add test to ensure url param values are all unique. export enum UrlParam { - Parameters = "a", - Items = "b", - EventType = "c", - UseFirebase = "d", - TimestampMicros = "e", - NonPersonalizedAds = "f", - EventData = "g", - FirebaseAppId = "h", - MeasurementId = "i", - EventName = "j", - APISecret = "k", - UserId = "l", - UserProperties = "m", - ClientId = "n", - AppInstanceId = "o", - Version = "p", - UseTextBox = "q", - Payload = "r", - PayloadObj = "s", - PayloadError = "t" + Parameters = "parameters", + Items = "items", + EventType = "event_type", + UseFirebase = "use_firebase", + TimestampMicros = "timestamp_micros", + NonPersonalizedAds = "non_personalized_ads", + EventData = "event_data", + FirebaseAppId = "firebase_app_id", + MeasurementId = "measurement_id", + EventName = "event_name", + APISecret = "api_secret", + UserId = "user_id", + UserProperties = "user_properties", + ClientId = "client_id", + AppInstanceId = "app_instance_id", + Version = "version", + UseTextBox = "use_text_box", + Payload = "payload", + PayloadObj = "payload_obj", + PayloadError = "payload_error", + IpOverride = "ip_override", + UserLocationCity = "user_location_city", + UserLocationRegionId = "user_location_region_id", + UserLocationCountryId = "user_location_country_id", + UserLocationSubcontinentId = "user_location_subcontinent_id", + UserLocationContinentId = "user_location_continent_id", } export enum ValidationStatus { @@ -153,4 +196,10 @@ export interface URLParts { [UrlParam.APISecret]?: string [UrlParam.TimestampMicros]?: string [UrlParam.NonPersonalizedAds]?: boolean + [UrlParam.IpOverride]?: string + [UrlParam.UserLocationCity]?: string + [UrlParam.UserLocationRegionId]?: string + [UrlParam.UserLocationCountryId]?: string + [UrlParam.UserLocationSubcontinentId]?: string + [UrlParam.UserLocationContinentId]?: string } diff --git a/src/components/ga4/EventBuilder/useInputs.ts b/src/components/ga4/EventBuilder/useInputs.ts index 85dd4cc5..37b29e76 100644 --- a/src/components/ga4/EventBuilder/useInputs.ts +++ b/src/components/ga4/EventBuilder/useInputs.ts @@ -14,7 +14,7 @@ const useInputs = (categories: Category[]) => { ) const [useTextBox, setUseTextBox] = useHydratedPersistantBoolean( - StorageKey.eventBuilderUseFirebase, + StorageKey.eventBuilderUseTextBox, UrlParam.UseTextBox, false ) @@ -78,6 +78,36 @@ const useInputs = (categories: Category[]) => { UrlParam.TimestampMicros ) + const [ip_override, setIpOverride] = useHydratedPersistantString( + StorageKey.ga4EventBuilderIpOverride, + UrlParam.IpOverride + ) + + const [user_location_city, setUserLocationCity] = useHydratedPersistantString( + StorageKey.ga4EventBuilderUserLocationCity, + UrlParam.UserLocationCity + ) + const [user_location_region_id, setUserLocationRegionId] = + useHydratedPersistantString( + StorageKey.ga4EventBuilderUserLocationRegionId, + UrlParam.UserLocationRegionId + ) + const [user_location_country_id, setUserLocationCountryId] = + useHydratedPersistantString( + StorageKey.ga4EventBuilderUserLocationCountryId, + UrlParam.UserLocationCountryId + ) + const [user_location_subcontinent_id, setUserLocationSubcontinentId] = + useHydratedPersistantString( + StorageKey.ga4EventBuilderUserLocationSubcontinentId, + UrlParam.UserLocationSubcontinentId + ) + const [user_location_continent_id, setUserLocationContinentId] = + useHydratedPersistantString( + StorageKey.ga4EventBuilderUserLocationContinentId, + UrlParam.UserLocationContinentId + ) + return { useFirebase, setUseFirebase, @@ -107,7 +137,19 @@ const useInputs = (categories: Category[]) => { setNonPersonalizedAds, timestamp_micros, setTimestampMicros, + ip_override, + setIpOverride, + user_location_city, + setUserLocationCity, + user_location_region_id, + setUserLocationRegionId, + user_location_country_id, + setUserLocationCountryId, + user_location_subcontinent_id, + setUserLocationSubcontinentId, + user_location_continent_id, + setUserLocationContinentId, } } -export default useInputs +export default useInputs \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 17578213..6b5ada34 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -243,6 +243,7 @@ export enum StorageKey { eventBuilderTimestampMicros = "ga4/event-builder/timestamp-micros", eventBuilderNonPersonalizedAds = "ga4/event-builder/non-personalized-ads", eventBuilderUseFirebase = "ga4/event-builder/use-firebase", + eventBuilderUseTextBox = "ga4/event-builder/use-text-box", ga4EventBuilderEvents = "ga4/event-builder/events", ga4EventBuilderLastEventType = "ga4/event-builder/last-event-type", ga4EventBuilderParameters = "ga4/event-builder/parameters", @@ -252,6 +253,12 @@ export enum StorageKey { ga4EventBuilderPayload = "ga4/event-builder/payload", ga4EventBuilderPayloadObj = "ga4/event-builder/payload-obj", ga4EventBuilderPayloadError = "ga4/event-builder/payload-error", + ga4EventBuilderIpOverride = "ga4/event-builder/ip-override", + ga4EventBuilderUserLocationCity = "ga4/event-builder/user-location-city", + ga4EventBuilderUserLocationRegionId = "ga4/event-builder/user-location-region-id", + ga4EventBuilderUserLocationCountryId = "ga4/event-builder/user-location-country-id", + ga4EventBuilderUserLocationSubcontinentId = "ga4/event-builder/user-location-subcontinent-id", + ga4EventBuilderUserLocationContinentId = "ga4/event-builder/user-location-continent-id", } export const EventAction = { From d0a2dd2a3c7f1a260a926178dea5e7e6ed71d7fc Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 28 Aug 2025 18:44:02 +0000 Subject: [PATCH 04/23] update ip_override description --- src/components/ga4/EventBuilder/GeographicInformation.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ga4/EventBuilder/GeographicInformation.tsx b/src/components/ga4/EventBuilder/GeographicInformation.tsx index 11b621da..de533973 100644 --- a/src/components/ga4/EventBuilder/GeographicInformation.tsx +++ b/src/components/ga4/EventBuilder/GeographicInformation.tsx @@ -134,8 +134,7 @@ const GeographicInformation: React.FC = ({ IP Override - Specify an IP address as an alternative to sending user location. This - IP address will be used to derive the user's geographic information. If + Provide an IP address to derive the user's geographic location. If both an IP override and user location are provided, user location will be used. From cbfa48e05418b577307fd5379f0360263165ab63 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 28 Aug 2025 19:43:18 +0000 Subject: [PATCH 05/23] revert formatting changes --- src/components/ga4/EventBuilder/index.tsx | 554 +++++++++++----------- 1 file changed, 284 insertions(+), 270 deletions(-) diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index e293cc83..67ae167e 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -132,7 +132,10 @@ export type EventPayload = { continent_id: string | undefined } } -export const EventCtx = React.createContext(undefined) +export const EventCtx = React.createContext< + | EventPayload + | undefined +>(undefined) export const ShowAdvancedCtx = React.createContext(false) export const UseFirebaseCtx = React.createContext(false) @@ -254,7 +257,7 @@ const EventBuilder: React.FC = () => { return ( Overview - + The GA4 Event Builder allows you to create, validate, and send events using the {ga4MeasurementProtocol}. @@ -295,75 +298,76 @@ const EventBuilder: React.FC = () => { After choosing a client, fill out the inputs below. - - {useFirebase ? ( - <> - - - - ) : ( - <> - - - - )} - - { + + {useFirebase ? ( + <> + + + + ) : ( + <> + + + + )} + + { + <> { -
+
- } - {useTextBox && ( + } + + { useTextBox && <> - 0 ? payloadObj : inputPayload - } - label={Label.Payload} - onChange={input => { + 0 ? payloadObj : inputPayload} + label={Label.Payload} + onChange={(input) => { setInputPayload(input) formatPayload() - }} - /> + } + } + /> -
+
-
+
- - format payload - + + format payload + - {payloadErrors && ( - - + { payloadErrors && ( + + - )} - - )} + )} + + } - {!useTextBox && ( -
-
- - data-testid={Label.EventCategory} + { !useTextBox && +
+
+ + + + data-testid={Label.EventCategory} + fullWidth + disableClearable + autoComplete + autoHighlight + autoSelect + options={Object.values(Category)} + getOptionLabel={category => category} + value={category} + onChange={(_event, value) => { + setCategory(value as Category) + const events = eventsForCategory(value as Category) + if (events.length > 0) { + setType(events[0].type) + } + }} + renderInput={params => ( + + )} + /> + {type === EventType.CustomEvent ? ( + { + setEventName(e.target.value) + }} + /> + ) : ( + + data-testid={Label.EventName} fullWidth disableClearable autoComplete autoHighlight autoSelect - options={Object.values(Category)} - getOptionLabel={category => category} - value={category} + options={eventsForCategory(category).map(e => e.type)} + getOptionLabel={eventType => eventType} + value={type} onChange={(_event, value) => { - setCategory(value as Category) - const events = eventsForCategory(value as Category) - if (events.length > 0) { - setType(events[0].type) - } + setType(value as EventType) }} renderInput={params => ( + The name of the event. + + } /> )} /> - {type === EventType.CustomEvent ? ( - { - setEventName(e.target.value) - }} - /> - ) : ( - - data-testid={Label.EventName} - fullWidth - disableClearable - autoComplete - autoHighlight - autoSelect - options={eventsForCategory(category).map(e => e.type)} - getOptionLabel={eventType => eventType} - value={type} - onChange={(_event, value) => { - setType(value as EventType) - }} - renderInput={params => ( - The name of the event.} - /> - )} - /> - )} - - { - setTimestampMicros( - (new Date().getTime() * 1000).toString() - ) - }} - > - - - - } - /> - - - Check to indicate events should not be used for personalized - ads. - - } - > - - {Label.NonPersonalizedAds} - - -
+ )} + + { + setTimestampMicros((new Date().getTime() * 1000).toString()) + }} + > + + + + } + /> - Event details - - Finally, specify the parameters to send with the event. By default, - only recommended parameters for the event will appear here. Check - "show advanced options" to add custom parameters, user properties, - or geographic information. - - - show advanced options - - -
- + Check to indicate events should not be used for personalized ads. + + } + > + - Parameters + {Label.NonPersonalizedAds} + + +
+ + Event details + + Finally, specify the parameters to send with the event. By default, only + recommended parameters for the event will appear here. Check "show + advanced options" to add custom parameters, user properties, or geographic + information. + + + show advanced options + + +
+ + Parameters + + {items !== undefined && ( + <> + Items + + + )} + {(showAdvanced || + (userProperties !== undefined && + userProperties.length !== 0)) && ( + <> + User properties - {items !== undefined && ( - <> - Items - - - )} - {(showAdvanced || - (userProperties !== undefined && - userProperties.length !== 0)) && ( - <> - User properties - - - )} - {showAdvanced && ( - <> - - - )} + + )} + {showAdvanced && ( + <> + + + )}
- )} + } Validate & Send event From c293e02d5ba7e54f2ca9378ea6552c3e27f402da Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 4 Sep 2025 19:36:04 +0000 Subject: [PATCH 06/23] revert ad_impression and screen_view changes --- .../EventBuilder/ValidateEvent/handlers/formatCheckLib.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts index 1fade516..b6a72d53 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts @@ -5,11 +5,12 @@ import {eventDefinitions} from "../schemas/eventTypes/eventDefinitions" import {InstanceId} from "../../types" const RESERVED_EVENT_NAMES = [ - "ad_activeview", "ad_click", "ad_exposure", "ad_query", + "ad_activeview", "ad_click", "ad_exposure", "ad_impression", "ad_query", "adunit_exposure", "app_clear_data", "app_install", "app_update", "app_remove", "error", "first_open", "first_visit", "in_app_purchase", "notification_dismiss", "notification_foreground", "notification_open", - "notification_receive", "os_update", "session_start", "user_engagement" + "notification_receive", "os_update", "screen_view", "session_start", + "user_engagement" ] const RESERVED_USER_PROPERTY_NAMES = [ "first_open_time", "first_visit_time", "last_deep_link_referrer", "user_id", From 21a48b8f9fd11ffee341191875af632a8d8892ed Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 5 Sep 2025 14:04:57 +0000 Subject: [PATCH 07/23] reorder form fields --- .../EventBuilder/GeographicInformation.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/ga4/EventBuilder/GeographicInformation.tsx b/src/components/ga4/EventBuilder/GeographicInformation.tsx index de533973..3102fafc 100644 --- a/src/components/ga4/EventBuilder/GeographicInformation.tsx +++ b/src/components/ga4/EventBuilder/GeographicInformation.tsx @@ -110,25 +110,25 @@ const GeographicInformation: React.FC = ({ setUserLocationSubcontinentId(e.target.value)} - helperText="The subcontinent of the user. E.g. '021'." + value={user_location_continent_id || ""} + onChange={e => setUserLocationContinentId(e.target.value)} + helperText="The continent of the user. E.g. '019'." /> setUserLocationContinentId(e.target.value)} - helperText="The continent of the user. E.g. '019'." + value={user_location_subcontinent_id || ""} + onChange={e => setUserLocationSubcontinentId(e.target.value)} + helperText="The subcontinent of the user. E.g. '021'." /> From 35c6ffe33d5a4e95abc9b11b72e6912a9d551e0c Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 5 Sep 2025 19:34:46 +0000 Subject: [PATCH 08/23] initial changes for device fields --- .../ga4/EventBuilder/DeviceInformation.tsx | 183 ++++++++++++++++++ .../ValidateEvent/schemas/baseContent.ts | 2 + .../ValidateEvent/schemas/deviceSchema.ts | 33 ++++ .../EventBuilder/ValidateEvent/usePayload.ts | 15 +- src/components/ga4/EventBuilder/index.tsx | 59 +++++- 5 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 src/components/ga4/EventBuilder/DeviceInformation.tsx create mode 100644 src/components/ga4/EventBuilder/ValidateEvent/schemas/deviceSchema.ts diff --git a/src/components/ga4/EventBuilder/DeviceInformation.tsx b/src/components/ga4/EventBuilder/DeviceInformation.tsx new file mode 100644 index 00000000..adcfd791 --- /dev/null +++ b/src/components/ga4/EventBuilder/DeviceInformation.tsx @@ -0,0 +1,183 @@ +import React from "react" +import Chip from "@mui/material/Chip" +import Divider from "@mui/material/Divider" +import { styled } from "@mui/material/styles" +import Typography from "@mui/material/Typography" +import TextField from "@mui/material/TextField" +import Grid from "@mui/material/Grid" + +import ExternalLink from "@/components/ExternalLink" + +const Root = styled("div")(({ theme }) => ({ + marginTop: theme.spacing(1), +})) + +type DeviceInformationProps = { + device_category: string | undefined + setDeviceCategory: (value: string) => void + device_language: string | undefined + setDeviceLanguage: (value: string) => void + device_screen_resolution: string | undefined + setDeviceScreenResolution: (value: string) => void + device_operating_system: string | undefined + setDeviceOperatingSystem: (value: string) => void + device_operating_system_version: string | undefined + setDeviceOperatingSystemVersion: (value: string) => void + device_model: string | undefined + setDeviceModel: (value: string) => void + device_brand: string | undefined + setDeviceBrand: (value: string) => void + device_browser: string | undefined + setDeviceBrowser: (value: string) => void + device_browser_version: string | undefined + setDeviceBrowserVersion: (value: string) => void +} + +const DeviceInformation: React.FC = ({ + device_category, + setDeviceCategory, + device_language, + setDeviceLanguage, + device_screen_resolution, + setDeviceScreenResolution, + device_operating_system, + setDeviceOperatingSystem, + device_operating_system_version, + setDeviceOperatingSystemVersion, + device_model, + setDeviceModel, + device_brand, + setDeviceBrand, + device_browser, + setDeviceBrowser, + device_browser_version, + setDeviceBrowserVersion, +}) => { + const docHref = + "https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference#device" + return ( + + + + + Device + + See the{" "} + documentation for more + information about device attributes. + + + + setDeviceCategory(e.target.value)} + helperText="The device category. E.g., 'mobile', 'desktop'." + /> + + + setDeviceLanguage(e.target.value)} + helperText="The language of the device. E.g., 'en-us'." + /> + + + setDeviceScreenResolution(e.target.value)} + helperText="The screen resolution of the device. E.g., '1920x1080'." + /> + + + setDeviceOperatingSystem(e.target.value)} + helperText="The operating system of the device. E.g., 'Windows'." + /> + + + setDeviceOperatingSystemVersion(e.target.value)} + helperText="The operating system version of the device. E.g., '10'." + /> + + + setDeviceModel(e.target.value)} + helperText="The device model. E.g., 'Pixel 6'." + /> + + + setDeviceBrand(e.target.value)} + helperText="The device brand. E.g., 'Google'." + /> + + + setDeviceBrowser(e.target.value)} + helperText="The browser name. E.g., 'Chrome'." + /> + + + setDeviceBrowserVersion(e.target.value)} + helperText="The browser version. E.g., '108.0.0.0'." + /> + + + + ) +} + +export default DeviceInformation \ No newline at end of file diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts index 0245e26b..f6307980 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts @@ -3,6 +3,7 @@ import { userPropertiesSchema } from './userProperties' import { eventsSchema } from './events' import { userLocationSchema } from "./userLocation" +import { deviceSchema } from "./deviceSchema" export const baseContentSchema = { type: "object", @@ -31,5 +32,6 @@ export const baseContentSchema = { ip_override: { type: "string", }, + device: deviceSchema, }, } \ No newline at end of file diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/deviceSchema.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/deviceSchema.ts new file mode 100644 index 00000000..4818bc09 --- /dev/null +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/deviceSchema.ts @@ -0,0 +1,33 @@ +export const deviceSchema = { + type: "object", + additionalProperties: false, + properties: { + category: { + type: "string", + }, + language: { + type: "string", + }, + screen_resolution: { + type: "string", + }, + operating_system: { + type: "string", + }, + operating_system_version: { + type: "string", + }, + model: { + type: "string", + }, + brand: { + type: "string", + }, + browser: { + type: "string", + }, + browser_version: { + type: "string", + }, + }, +}; diff --git a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts index 8b7423a2..712a2d4c 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts @@ -74,6 +74,7 @@ const usePayload = (): {} => { payloadObj, ip_override, user_location, + device } = useContext(EventCtx)! const eventName = useMemo(() => { @@ -116,13 +117,24 @@ const usePayload = (): {} => { return cleaned_location }, [user_location]) + const device_info = useMemo(() => { + if (device === undefined) { + return undefined + } + const cleaned_device = removeUndefined(device) + if (Object.keys(cleaned_device).length === 0) { + return undefined + } + return cleaned_device + }, [device]) + let payload = useMemo(() => { return { ...removeUndefined(clientIds), ...removeUndefined({ timestamp_micros }), ...removeUndefined({ non_personalized_ads }), ...removeUndefined(removeEmptyObject({ user_properties })), - ...removeUndefined({ ip_override, user_location: final_user_location }), + ...removeUndefined({ ip_override, user_location: final_user_location, device: device_info }), events: [ { name: eventName, ...(parameters.length > 0 ? { params } : {}) }, ], @@ -137,6 +149,7 @@ const usePayload = (): {} => { user_properties, ip_override, final_user_location, + final_device_info ]) if (useTextBox) { diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 67ae167e..91169281 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -44,6 +44,7 @@ import ValidateEvent from "./ValidateEvent" import { PlainButton } from "@/components/Buttons" import { useEffect } from "react" import GeographicInformation from "./GeographicInformation"; +import DeviceInformation from "./DeviceInformation"; const PREFIX = 'EventBuilder'; @@ -131,6 +132,17 @@ export type EventPayload = { subcontinent_id: string | undefined continent_id: string | undefined } + device: { + category: string | undefined + language: string | undefined + screen_resolution: string | undefined + operating_system: string | undefined + operating_system_version: string | undefined + model: string | undefined + brand: string | undefined + browser: string | undefined + browser_version: string | undefined + } } export const EventCtx = React.createContext< | EventPayload @@ -214,9 +226,19 @@ const EventBuilder: React.FC = () => { user_location_subcontinent_id, setUserLocationSubcontinentId, user_location_continent_id, - setUserLocationContinentId, + setUserLocationContinentId } = useInputs(categories) + const [device_category, setDeviceCategory] = React.useState("") + const [device_language, setDeviceLanguage] = React.useState("") + const [device_screen_resolution, setDeviceScreenResolution] = React.useState("") + const [device_operating_system, setDeviceOperatingSystem] = React.useState("") + const [device_operating_system_version, setDeviceOperatingSystemVersion] = React.useState("") + const [device_model, setDeviceModel] = React.useState("") + const [device_brand, setDeviceBrand] = React.useState("") + const [device_browser, setDeviceBrowser] = React.useState("") + const [device_browser_version, setDeviceBrowserVersion] = React.useState("") + const formatPayload = React.useCallback(() => { try { if (inputPayload) { @@ -628,6 +650,30 @@ const EventBuilder: React.FC = () => { ip_override={ip_override} setIpOverride={setIpOverride} /> + )} @@ -662,6 +708,17 @@ const EventBuilder: React.FC = () => { subcontinent_id: user_location_subcontinent_id, continent_id: user_location_continent_id, }, + device: { + category: device_category, + language: device_language, + screen_resolution: device_screen_resolution, + operating_system: device_operating_system, + operating_system_version: device_operating_system_version, + model: device_model, + brand: device_brand, + browser: device_browser, + browser_version: device_browser_version, + }, }} > Date: Fri, 5 Sep 2025 19:45:45 +0000 Subject: [PATCH 09/23] simplify implementation --- .../EventBuilder/ValidateEvent/usePayload.ts | 6 +-- .../ValidateEvent/useSharableLink.ts | 15 +------ src/components/ga4/EventBuilder/index.tsx | 38 ++++------------- src/components/ga4/EventBuilder/useInputs.ts | 42 ------------------- src/constants.ts | 6 --- 5 files changed, 12 insertions(+), 95 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts index 8b7423a2..854cf155 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts @@ -105,7 +105,7 @@ const usePayload = (): {} => { [userProperties] ) - const final_user_location = useMemo(() => { + const user_location_info = useMemo(() => { if (user_location === undefined) { return undefined } @@ -122,7 +122,7 @@ const usePayload = (): {} => { ...removeUndefined({ timestamp_micros }), ...removeUndefined({ non_personalized_ads }), ...removeUndefined(removeEmptyObject({ user_properties })), - ...removeUndefined({ ip_override, user_location: final_user_location }), + ...removeUndefined({ ip_override, user_location: user_location_info }), events: [ { name: eventName, ...(parameters.length > 0 ? { params } : {}) }, ], @@ -136,7 +136,7 @@ const usePayload = (): {} => { timestamp_micros, user_properties, ip_override, - final_user_location, + user_location_info, ]) if (useTextBox) { diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts index feccefae..65c2ac47 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts @@ -61,17 +61,6 @@ const useSharableLink = () => { addIfTruthy(UrlParam.IpOverride, ip_override) - if (user_location) { - addIfTruthy(UrlParam.UserLocationCity, user_location.city) - addIfTruthy(UrlParam.UserLocationRegionId, user_location.region_id) - addIfTruthy(UrlParam.UserLocationCountryId, user_location.country_id) - addIfTruthy( - UrlParam.UserLocationSubcontinentId, - user_location.subcontinent_id - ) - addIfTruthy(UrlParam.UserLocationContinentId, user_location.continent_id) - } - if (userProperties) { params.append(UrlParam.UserProperties, encodeObject(userProperties)) } @@ -98,9 +87,7 @@ const useSharableLink = () => { instanceId, api_secret, timestamp_micros, - non_personalized_ads, - ip_override, - user_location, + non_personalized_ads ]) } diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 67ae167e..e7f12ea6 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -202,21 +202,16 @@ const EventBuilder: React.FC = () => { timestamp_micros, setTimestampMicros, non_personalized_ads, - setNonPersonalizedAds, - ip_override, - setIpOverride, - user_location_city, - setUserLocationCity, - user_location_region_id, - setUserLocationRegionId, - user_location_country_id, - setUserLocationCountryId, - user_location_subcontinent_id, - setUserLocationSubcontinentId, - user_location_continent_id, - setUserLocationContinentId, + setNonPersonalizedAds } = useInputs(categories) + const [user_location_city, setUserLocationCity] = React.useState("") + const [user_location_region_id, setUserLocationRegionId] = React.useState("") + const [user_location_country_id, setUserLocationCountryId] = React.useState("") + const [user_location_subcontinent_id, setUserLocationSubcontinentId] = React.useState("") + const [user_location_continent_id, setUserLocationContinentId] = React.useState("") + const [ip_override, setIpOverride] = React.useState("") + const formatPayload = React.useCallback(() => { try { if (inputPayload) { @@ -237,23 +232,6 @@ const EventBuilder: React.FC = () => { formatPayload() }, [inputPayload, formatPayload]) - useEffect(() => { - // Clear geographic information on page load. - setIpOverride("") - setUserLocationCity("") - setUserLocationRegionId("") - setUserLocationCountryId("") - setUserLocationSubcontinentId("") - setUserLocationContinentId("") - }, [ - setIpOverride, - setUserLocationCity, - setUserLocationRegionId, - setUserLocationCountryId, - setUserLocationSubcontinentId, - setUserLocationContinentId, - ]) - return ( Overview diff --git a/src/components/ga4/EventBuilder/useInputs.ts b/src/components/ga4/EventBuilder/useInputs.ts index 37b29e76..f585201f 100644 --- a/src/components/ga4/EventBuilder/useInputs.ts +++ b/src/components/ga4/EventBuilder/useInputs.ts @@ -78,36 +78,6 @@ const useInputs = (categories: Category[]) => { UrlParam.TimestampMicros ) - const [ip_override, setIpOverride] = useHydratedPersistantString( - StorageKey.ga4EventBuilderIpOverride, - UrlParam.IpOverride - ) - - const [user_location_city, setUserLocationCity] = useHydratedPersistantString( - StorageKey.ga4EventBuilderUserLocationCity, - UrlParam.UserLocationCity - ) - const [user_location_region_id, setUserLocationRegionId] = - useHydratedPersistantString( - StorageKey.ga4EventBuilderUserLocationRegionId, - UrlParam.UserLocationRegionId - ) - const [user_location_country_id, setUserLocationCountryId] = - useHydratedPersistantString( - StorageKey.ga4EventBuilderUserLocationCountryId, - UrlParam.UserLocationCountryId - ) - const [user_location_subcontinent_id, setUserLocationSubcontinentId] = - useHydratedPersistantString( - StorageKey.ga4EventBuilderUserLocationSubcontinentId, - UrlParam.UserLocationSubcontinentId - ) - const [user_location_continent_id, setUserLocationContinentId] = - useHydratedPersistantString( - StorageKey.ga4EventBuilderUserLocationContinentId, - UrlParam.UserLocationContinentId - ) - return { useFirebase, setUseFirebase, @@ -137,18 +107,6 @@ const useInputs = (categories: Category[]) => { setNonPersonalizedAds, timestamp_micros, setTimestampMicros, - ip_override, - setIpOverride, - user_location_city, - setUserLocationCity, - user_location_region_id, - setUserLocationRegionId, - user_location_country_id, - setUserLocationCountryId, - user_location_subcontinent_id, - setUserLocationSubcontinentId, - user_location_continent_id, - setUserLocationContinentId, } } diff --git a/src/constants.ts b/src/constants.ts index 6b5ada34..48d869f6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -253,12 +253,6 @@ export enum StorageKey { ga4EventBuilderPayload = "ga4/event-builder/payload", ga4EventBuilderPayloadObj = "ga4/event-builder/payload-obj", ga4EventBuilderPayloadError = "ga4/event-builder/payload-error", - ga4EventBuilderIpOverride = "ga4/event-builder/ip-override", - ga4EventBuilderUserLocationCity = "ga4/event-builder/user-location-city", - ga4EventBuilderUserLocationRegionId = "ga4/event-builder/user-location-region-id", - ga4EventBuilderUserLocationCountryId = "ga4/event-builder/user-location-country-id", - ga4EventBuilderUserLocationSubcontinentId = "ga4/event-builder/user-location-subcontinent-id", - ga4EventBuilderUserLocationContinentId = "ga4/event-builder/user-location-continent-id", } export const EventAction = { From 34ed75dc393550fec98eccee98acd5c502f0f96a Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 5 Sep 2025 19:52:10 +0000 Subject: [PATCH 10/23] continue simplify implementation, cleanup files --- .../ValidateEvent/useSharableLink.ts | 6 +- src/components/ga4/EventBuilder/types.ts | 60 +++++++------------ src/components/ga4/EventBuilder/useInputs.ts | 2 +- 3 files changed, 22 insertions(+), 46 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts index 65c2ac47..dd2a63ab 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts @@ -17,8 +17,6 @@ const useSharableLink = () => { parameters, eventName, type, - ip_override, - user_location, } = useContext(EventCtx)! return useMemo(() => { @@ -59,8 +57,6 @@ const useSharableLink = () => { addIfTruthy(UrlParam.TimestampMicros, timestamp_micros) - addIfTruthy(UrlParam.IpOverride, ip_override) - if (userProperties) { params.append(UrlParam.UserProperties, encodeObject(userProperties)) } @@ -87,7 +83,7 @@ const useSharableLink = () => { instanceId, api_secret, timestamp_micros, - non_personalized_ads + non_personalized_ads, ]) } diff --git a/src/components/ga4/EventBuilder/types.ts b/src/components/ga4/EventBuilder/types.ts index f9aeeb1b..15366775 100644 --- a/src/components/ga4/EventBuilder/types.ts +++ b/src/components/ga4/EventBuilder/types.ts @@ -111,44 +111,30 @@ export enum Label { Affiliation = "#/events/0/params/affiliation", Shipping = "#/events/0/params/shipping", Tax = "#/events/0/params/tax", - - // Geographic Information - IpOverride = "ip address", - City = "city", - RegionId = "region id", - CountryId = "country id", - SubcontinentId = "subcontinent id", - ContinentId = "continent id", } // TODO - Add test to ensure url param values are all unique. export enum UrlParam { - Parameters = "parameters", - Items = "items", - EventType = "event_type", - UseFirebase = "use_firebase", - TimestampMicros = "timestamp_micros", - NonPersonalizedAds = "non_personalized_ads", - EventData = "event_data", - FirebaseAppId = "firebase_app_id", - MeasurementId = "measurement_id", - EventName = "event_name", - APISecret = "api_secret", - UserId = "user_id", - UserProperties = "user_properties", - ClientId = "client_id", - AppInstanceId = "app_instance_id", - Version = "version", - UseTextBox = "use_text_box", - Payload = "payload", - PayloadObj = "payload_obj", - PayloadError = "payload_error", - IpOverride = "ip_override", - UserLocationCity = "user_location_city", - UserLocationRegionId = "user_location_region_id", - UserLocationCountryId = "user_location_country_id", - UserLocationSubcontinentId = "user_location_subcontinent_id", - UserLocationContinentId = "user_location_continent_id", + Parameters = "a", + Items = "b", + EventType = "c", + UseFirebase = "d", + TimestampMicros = "e", + NonPersonalizedAds = "f", + EventData = "g", + FirebaseAppId = "h", + MeasurementId = "i", + EventName = "j", + APISecret = "k", + UserId = "l", + UserProperties = "m", + ClientId = "n", + AppInstanceId = "o", + Version = "p", + UseTextBox = "q", + Payload = "r", + PayloadObj = "s", + PayloadError = "t" } export enum ValidationStatus { @@ -196,10 +182,4 @@ export interface URLParts { [UrlParam.APISecret]?: string [UrlParam.TimestampMicros]?: string [UrlParam.NonPersonalizedAds]?: boolean - [UrlParam.IpOverride]?: string - [UrlParam.UserLocationCity]?: string - [UrlParam.UserLocationRegionId]?: string - [UrlParam.UserLocationCountryId]?: string - [UrlParam.UserLocationSubcontinentId]?: string - [UrlParam.UserLocationContinentId]?: string } diff --git a/src/components/ga4/EventBuilder/useInputs.ts b/src/components/ga4/EventBuilder/useInputs.ts index f585201f..f8b41385 100644 --- a/src/components/ga4/EventBuilder/useInputs.ts +++ b/src/components/ga4/EventBuilder/useInputs.ts @@ -110,4 +110,4 @@ const useInputs = (categories: Category[]) => { } } -export default useInputs \ No newline at end of file +export default useInputs From ada804704dbe50780803b987a0da08ee23319023 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 5 Sep 2025 20:00:14 +0000 Subject: [PATCH 11/23] add back labels for geo info --- src/components/ga4/EventBuilder/types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/ga4/EventBuilder/types.ts b/src/components/ga4/EventBuilder/types.ts index 15366775..763ac785 100644 --- a/src/components/ga4/EventBuilder/types.ts +++ b/src/components/ga4/EventBuilder/types.ts @@ -111,6 +111,14 @@ export enum Label { Affiliation = "#/events/0/params/affiliation", Shipping = "#/events/0/params/shipping", Tax = "#/events/0/params/tax", + + // Geographic Information + IpOverride = "ip address", + City = "city", + RegionId = "region id", + CountryId = "country id", + SubcontinentId = "subcontinent id", + ContinentId = "continent id", } // TODO - Add test to ensure url param values are all unique. From 8bf9f1ba264dca4c8af8d11e307c489d7cce9f40 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 5 Sep 2025 20:06:18 +0000 Subject: [PATCH 12/23] additional file cleanup --- .../EventBuilder/GeographicInformation.tsx | 17 +---- .../schemas/userLocation.spec.ts | 76 ------------------- .../ValidateEvent/schemas/userLocation.ts | 2 +- src/components/ga4/EventBuilder/index.tsx | 2 +- 4 files changed, 3 insertions(+), 94 deletions(-) delete mode 100644 src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts diff --git a/src/components/ga4/EventBuilder/GeographicInformation.tsx b/src/components/ga4/EventBuilder/GeographicInformation.tsx index 3102fafc..364b0d0a 100644 --- a/src/components/ga4/EventBuilder/GeographicInformation.tsx +++ b/src/components/ga4/EventBuilder/GeographicInformation.tsx @@ -1,17 +1,3 @@ -// Copyright 2020 Google Inc. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - import React from "react" import Chip from "@mui/material/Chip"; import Divider from '@mui/material/Divider'; @@ -28,7 +14,6 @@ const Root = styled("div")(({ theme }) => ({ marginTop: theme.spacing(3), })) - interface GeographicInformationProps { user_location_city: string | undefined setUserLocationCity: (value: string) => void @@ -151,4 +136,4 @@ const GeographicInformation: React.FC = ({ ) } -export default GeographicInformation \ No newline at end of file +export default GeographicInformation diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts deleted file mode 100644 index 5384f3fa..00000000 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import "jest" -import { Validator } from "../validator" -import { userLocationSchema } from "./userLocation" - -describe("userLocationSchema", () => { - test("can be used to validate a valid payload", () => { - const validInput = { - city: "Mountain View", - region_id: "US-CA", - country_id: "US", - subcontinent_id: "021", - continent_id: "019", - } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(validInput)).toEqual(true) - }) - - test.each([["US-CA"], ["US-C"], ["US-C1A"]])( - "is valid with a valid region_id: %s", - region_id => { - const validator = new Validator(userLocationSchema) - expect(validator.isValid({ region_id })).toEqual(true) - } - ) - - test("is invalid with an invalid region_id", () => { - const invalidInput = { region_id: 123 } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(invalidInput)).toEqual(false) - }) - - test("is valid with a valid country_id", () => { - const validInput = { country_id: "US" } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(validInput)).toEqual(true) - }) - - test("is invalid with an invalid country_id", () => { - const invalidInput = { country_id: 1 } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(invalidInput)).toEqual(false) - }) - - test("is valid with a valid subcontinent_id", () => { - const validInput = { subcontinent_id: "021" } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(validInput)).toEqual(true) - }) - - test("is invalid with an invalid subcontinent_id", () => { - const validInput = { subcontinent_id: 541 } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(validInput)).toEqual(false) - }) - - test("is valid with a valid continent_id", () => { - const validInput = { continent_id: "019" } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(validInput)).toEqual(true) - }) - - test("is invalid with an invalid continent_id", () => { - const validInput = { continent_id: 540 } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(validInput)).toEqual(false) - }) - - test("is invalid with additional properties", () => { - const invalidInput = { - city: "Mountain View", - extra_prop: "should fail", - } - const validator = new Validator(userLocationSchema) - expect(validator.isValid(invalidInput)).toEqual(false) - }) -}) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts index 312c15d6..799db137 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/userLocation.ts @@ -1,4 +1,4 @@ -// User Location Schema +// User location schema export const userLocationSchema = { type: "object", diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index e7f12ea6..c7cd38aa 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -202,7 +202,7 @@ const EventBuilder: React.FC = () => { timestamp_micros, setTimestampMicros, non_personalized_ads, - setNonPersonalizedAds + setNonPersonalizedAds, } = useInputs(categories) const [user_location_city, setUserLocationCity] = React.useState("") From 91fe7b0115e757d3e405555bc4f028f62fee27ac Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 5 Sep 2025 20:49:17 +0000 Subject: [PATCH 13/23] cleanup form descriptions and labels --- .../ga4/EventBuilder/DeviceInformation.tsx | 37 ++++++++++--------- src/components/ga4/EventBuilder/index.tsx | 1 - src/components/ga4/EventBuilder/types.ts | 11 ++++++ 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/components/ga4/EventBuilder/DeviceInformation.tsx b/src/components/ga4/EventBuilder/DeviceInformation.tsx index adcfd791..99101879 100644 --- a/src/components/ga4/EventBuilder/DeviceInformation.tsx +++ b/src/components/ga4/EventBuilder/DeviceInformation.tsx @@ -7,6 +7,7 @@ import TextField from "@mui/material/TextField" import Grid from "@mui/material/Grid" import ExternalLink from "@/components/ExternalLink" +import { Label } from "./types" const Root = styled("div")(({ theme }) => ({ marginTop: theme.spacing(1), @@ -71,108 +72,108 @@ const DeviceInformation: React.FC = ({ setDeviceCategory(e.target.value)} - helperText="The device category. E.g., 'mobile', 'desktop'." + helperText="The category of the device, e.g., 'mobile', 'desktop'" /> setDeviceLanguage(e.target.value)} - helperText="The language of the device. E.g., 'en-us'." + helperText="The language of the device in ISO 639-1 format, e.g., 'en', 'en-us'" /> setDeviceScreenResolution(e.target.value)} - helperText="The screen resolution of the device. E.g., '1920x1080'." + helperText="The resolution of the device, e.g., '1920x1080'." /> setDeviceOperatingSystem(e.target.value)} - helperText="The operating system of the device. E.g., 'Windows'." + helperText="The operating system of the device, e.g., 'MacOS', 'Windows'." /> setDeviceOperatingSystemVersion(e.target.value)} - helperText="The operating system version of the device. E.g., '10'." + helperText="The version of the device's operating system, e.g., '13.5'." /> setDeviceModel(e.target.value)} - helperText="The device model. E.g., 'Pixel 6'." + helperText="The model of the device, e.g., 'Pixel 6'." /> setDeviceBrand(e.target.value)} - helperText="The device brand. E.g., 'Google'." + helperText="The brand of the device, e.g., 'Google'." /> setDeviceBrowser(e.target.value)} - helperText="The browser name. E.g., 'Chrome'." + helperText="The brand or type of browser, e.g., 'Chrome'." /> setDeviceBrowserVersion(e.target.value)} - helperText="The browser version. E.g., '108.0.0.0'." + helperText="The version of the browser, e.g., '136.0.7103.60, 5.0'." /> diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 9fce7606..b6265596 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -217,7 +217,6 @@ const EventBuilder: React.FC = () => { setNonPersonalizedAds, } = useInputs(categories) - const [user_location_city, setUserLocationCity] = React.useState("") const [user_location_region_id, setUserLocationRegionId] = React.useState("") const [user_location_country_id, setUserLocationCountryId] = React.useState("") diff --git a/src/components/ga4/EventBuilder/types.ts b/src/components/ga4/EventBuilder/types.ts index 763ac785..c91af159 100644 --- a/src/components/ga4/EventBuilder/types.ts +++ b/src/components/ga4/EventBuilder/types.ts @@ -119,6 +119,17 @@ export enum Label { CountryId = "country id", SubcontinentId = "subcontinent id", ContinentId = "continent id", + + // Device Information + DeviceCategory = "device category", + DeviceLanguage = "device language", + DeviceScreenResolution = "device screen resolution", + DeviceOperatingSystem = "device operating system", + DeviceOperatingSystemVersion = "device operating system version", + DeviceModel = "device model", + DeviceBrand = "device brand", + DeviceBrowser = "device browser", + DeviceBrowserVersion = "device browser version", } // TODO - Add test to ensure url param values are all unique. From d051c96d0e2dd1781e202a98dd7f0038cb0991b3 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Mon, 8 Sep 2025 21:39:40 +0000 Subject: [PATCH 14/23] add user_agent when client is gtag --- .../ga4/EventBuilder/DeviceInformation.tsx | 35 +++++++++++++++++-- .../ValidateEvent/schemas/baseContent.ts | 3 ++ src/components/ga4/EventBuilder/index.tsx | 7 ++-- src/components/ga4/EventBuilder/types.ts | 1 + 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/components/ga4/EventBuilder/DeviceInformation.tsx b/src/components/ga4/EventBuilder/DeviceInformation.tsx index 99101879..bf8e6757 100644 --- a/src/components/ga4/EventBuilder/DeviceInformation.tsx +++ b/src/components/ga4/EventBuilder/DeviceInformation.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { useContext } from "react" import Chip from "@mui/material/Chip" import Divider from "@mui/material/Divider" import { styled } from "@mui/material/styles" @@ -8,6 +8,7 @@ import Grid from "@mui/material/Grid" import ExternalLink from "@/components/ExternalLink" import { Label } from "./types" +import { UseFirebaseCtx } from "." const Root = styled("div")(({ theme }) => ({ marginTop: theme.spacing(1), @@ -32,6 +33,8 @@ type DeviceInformationProps = { setDeviceBrowser: (value: string) => void device_browser_version: string | undefined setDeviceBrowserVersion: (value: string) => void + user_agent: string | undefined + setUserAgent: (value: string) => void } const DeviceInformation: React.FC = ({ @@ -53,7 +56,10 @@ const DeviceInformation: React.FC = ({ setDeviceBrowser, device_browser_version, setDeviceBrowserVersion, + user_agent, + setUserAgent, }) => { + const useFirebase = useContext(UseFirebaseCtx) const docHref = "https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference#device" return ( @@ -61,7 +67,7 @@ const DeviceInformation: React.FC = ({ - Device + Device Attributes See the{" "} documentation for more @@ -89,7 +95,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_language || ""} onChange={e => setDeviceLanguage(e.target.value)} - helperText="The language of the device in ISO 639-1 format, e.g., 'en', 'en-us'" + helperText="The language of the device in ISO 639-1 format, e.g., 'en'" /> @@ -177,6 +183,29 @@ const DeviceInformation: React.FC = ({ /> + {!useFirebase && ( + <> + User Agent + + Specify a user agent string for Google Analytics to use to derive device information. + This field is ignored if device information is provided. + + + + setUserAgent(e.target.value)} + helperText="The user agent string." + /> + + + + )} ) } diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts index f6307980..8ce78c0b 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts @@ -33,5 +33,8 @@ export const baseContentSchema = { type: "string", }, device: deviceSchema, + user_agent: { + type: "string", + } }, } \ No newline at end of file diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index b6265596..9c5bf5b1 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -233,6 +233,7 @@ const EventBuilder: React.FC = () => { const [device_brand, setDeviceBrand] = React.useState("") const [device_browser, setDeviceBrowser] = React.useState("") const [device_browser_version, setDeviceBrowserVersion] = React.useState("") + const [user_agent, setUserAgent] = React.useState("") const formatPayload = React.useCallback(() => { try { @@ -609,7 +610,7 @@ const EventBuilder: React.FC = () => { )} {showAdvanced && ( - <> + { setDeviceBrowser={setDeviceBrowser} device_browser_version={device_browser_version} setDeviceBrowserVersion={setDeviceBrowserVersion} + user_agent={user_agent} + setUserAgent={setUserAgent} /> - + )}
diff --git a/src/components/ga4/EventBuilder/types.ts b/src/components/ga4/EventBuilder/types.ts index c91af159..baa3d36d 100644 --- a/src/components/ga4/EventBuilder/types.ts +++ b/src/components/ga4/EventBuilder/types.ts @@ -130,6 +130,7 @@ export enum Label { DeviceBrand = "device brand", DeviceBrowser = "device browser", DeviceBrowserVersion = "device browser version", + UserAgent = "user agent", } // TODO - Add test to ensure url param values are all unique. From 5969cf3f07ffcc48c351b72c83873f2aa38526fc Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Tue, 9 Sep 2025 19:45:52 +0000 Subject: [PATCH 15/23] revert changes for useTextBox persistence fix, moreved to separate PR --- src/components/ga4/EventBuilder/useInputs.ts | 2 +- src/constants.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ga4/EventBuilder/useInputs.ts b/src/components/ga4/EventBuilder/useInputs.ts index f8b41385..85dd4cc5 100644 --- a/src/components/ga4/EventBuilder/useInputs.ts +++ b/src/components/ga4/EventBuilder/useInputs.ts @@ -14,7 +14,7 @@ const useInputs = (categories: Category[]) => { ) const [useTextBox, setUseTextBox] = useHydratedPersistantBoolean( - StorageKey.eventBuilderUseTextBox, + StorageKey.eventBuilderUseFirebase, UrlParam.UseTextBox, false ) diff --git a/src/constants.ts b/src/constants.ts index 48d869f6..17578213 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -243,7 +243,6 @@ export enum StorageKey { eventBuilderTimestampMicros = "ga4/event-builder/timestamp-micros", eventBuilderNonPersonalizedAds = "ga4/event-builder/non-personalized-ads", eventBuilderUseFirebase = "ga4/event-builder/use-firebase", - eventBuilderUseTextBox = "ga4/event-builder/use-text-box", ga4EventBuilderEvents = "ga4/event-builder/events", ga4EventBuilderLastEventType = "ga4/event-builder/last-event-type", ga4EventBuilderParameters = "ga4/event-builder/parameters", From 7f86232b017ba9d75585597d124bda0ed0f447ce Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Wed, 10 Sep 2025 15:13:25 +0000 Subject: [PATCH 16/23] add user_agent field to payload --- .../ga4/EventBuilder/ValidateEvent/usePayload.ts | 8 +++++--- src/components/ga4/EventBuilder/index.tsx | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts index a174adb1..236653f5 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts @@ -74,7 +74,8 @@ const usePayload = (): {} => { payloadObj, ip_override, user_location, - device + device, + user_agent } = useContext(EventCtx)! const eventName = useMemo(() => { @@ -134,7 +135,7 @@ const usePayload = (): {} => { ...removeUndefined({ timestamp_micros }), ...removeUndefined({ non_personalized_ads }), ...removeUndefined(removeEmptyObject({ user_properties })), - ...removeUndefined({ ip_override, user_location: user_location_info, device: device_info }), + ...removeUndefined({ ip_override, user_location: user_location_info, device: device_info, user_agent }), events: [ { name: eventName, ...(parameters.length > 0 ? { params } : {}) }, ], @@ -149,7 +150,8 @@ const usePayload = (): {} => { user_properties, ip_override, user_location_info, - device_info + device_info, + user_agent ]) if (useTextBox) { diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 88b9d49e..010bc795 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -125,6 +125,7 @@ export type EventPayload = { useTextBox: boolean payloadObj: any ip_override: string | undefined + user_agent: string | undefined user_location: { city: string | undefined region_id: string | undefined @@ -703,6 +704,7 @@ const EventBuilder: React.FC = () => { payloadObj, instanceId: useFirebase ? { firebase_app_id } : { measurement_id }, api_secret: api_secret!, + user_agent, ip_override, user_location: { city: user_location_city, From 6a28bb26e91124331c5a1951604e06c1e1db2569 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Mon, 15 Sep 2025 21:25:08 +0000 Subject: [PATCH 17/23] add ip_override and user_location to test --- .../ga4/EventBuilder/ValidateEvent/index.spec.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx b/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx index 0ed90e1a..7df63e0b 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx +++ b/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx @@ -53,6 +53,14 @@ const renderComponent = (props: Partial = {}) => { payloadObj: [], api_secret: "secret123", clientIds: {}, + ip_override: "", + user_location: { + city: "Mountain View", + region_id: "CA", + country_id: "US", + subcontinent_id: "021", + continent_id: "019" + } } return render( From 6f4815291cba1f792efd45622d46af1264d39fe7 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Mon, 15 Sep 2025 21:27:00 +0000 Subject: [PATCH 18/23] add device and user_agent fields to test --- .../ga4/EventBuilder/ValidateEvent/index.spec.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx b/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx index 7df63e0b..1b0e3207 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx +++ b/src/components/ga4/EventBuilder/ValidateEvent/index.spec.tsx @@ -60,7 +60,19 @@ const renderComponent = (props: Partial = {}) => { country_id: "US", subcontinent_id: "021", continent_id: "019" - } + }, + user_agent: "", + device: { + category: "mobile", + language: "en", + screen_resolution: "1280x2856", + operating_system: "Android", + operating_system_version: "14", + model: "Pixel 9 Pro", + brand: "Google", + browser: "Chrome", + browser_version: "136.0.7103.60" + } } return render( From 20ac90b3f5c99af4aaec826e8373e8e77d3bb5db Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 18 Sep 2025 15:22:03 +0000 Subject: [PATCH 19/23] remove unnecessary else --- src/components/ga4/EventBuilder/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 8817f162..6a062faa 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -251,9 +251,6 @@ const EventBuilder: React.FC = () => { setPayloadErrors("Empty Payload") setPayloadObj(JSON.stringify(payload, null, "\t")) setPayloadErrors("") - } else { - setPayloadErrors("Empty Payload") - setPayloadObj({}) } } catch (err: any) { setPayloadErrors(err.message) From d635c8833b971c29bf68dae240495b9840db4238 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 18 Sep 2025 15:25:58 +0000 Subject: [PATCH 20/23] fix merge conflict update --- src/components/ga4/EventBuilder/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 6a062faa..530ac169 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -249,7 +249,7 @@ const EventBuilder: React.FC = () => { setPayloadErrors("") } else { setPayloadErrors("Empty Payload") - setPayloadObj(JSON.stringify(payload, null, "\t")) + setPayloadObj({}) setPayloadErrors("") } } catch (err: any) { From 207bbd89a74e74786ccb7fdb138a5903023a3356 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 18 Sep 2025 15:29:27 +0000 Subject: [PATCH 21/23] resolve conflict issues --- .../ga4/EventBuilder/GeographicInformation.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ga4/EventBuilder/GeographicInformation.tsx b/src/components/ga4/EventBuilder/GeographicInformation.tsx index 5c93c1e5..4cb03455 100644 --- a/src/components/ga4/EventBuilder/GeographicInformation.tsx +++ b/src/components/ga4/EventBuilder/GeographicInformation.tsx @@ -64,7 +64,7 @@ const GeographicInformation: React.FC = ({ size="small" value={user_location_city || ""} onChange={e => setUserLocationCity(e.target.value)} - helperText="The city of the user. E.g. 'Mountain View'." + helperText="The city name, e.g., Mountain View" /> @@ -76,7 +76,7 @@ const GeographicInformation: React.FC = ({ size="small" value={user_location_region_id || ""} onChange={e => setUserLocationRegionId(e.target.value)} - helperText="The region of the user. E.g. 'US-CA'." + helperText="The country and subdivision, e.g., US-CA" /> @@ -88,7 +88,7 @@ const GeographicInformation: React.FC = ({ size="small" value={user_location_country_id || ""} onChange={e => setUserLocationCountryId(e.target.value)} - helperText="The country of the user. E.g. 'US'." + helperText="The country code, e.g., US" /> @@ -100,7 +100,7 @@ const GeographicInformation: React.FC = ({ size="small" value={user_location_continent_id || ""} onChange={e => setUserLocationContinentId(e.target.value)} - helperText="The continent of the user. E.g. '019'." + helperText="The continent code, e.g., 019" /> @@ -112,7 +112,7 @@ const GeographicInformation: React.FC = ({ size="small" value={user_location_subcontinent_id || ""} onChange={e => setUserLocationSubcontinentId(e.target.value)} - helperText="The subcontinent of the user. E.g. '021'." + helperText="The subcontinent code, e.g., 021" /> From 263342af1d57378fc142f6ce7c81fd7229759a0f Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 19 Sep 2025 17:19:51 +0000 Subject: [PATCH 22/23] update helper text --- .../ga4/EventBuilder/DeviceInformation.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/ga4/EventBuilder/DeviceInformation.tsx b/src/components/ga4/EventBuilder/DeviceInformation.tsx index bf8e6757..68df3492 100644 --- a/src/components/ga4/EventBuilder/DeviceInformation.tsx +++ b/src/components/ga4/EventBuilder/DeviceInformation.tsx @@ -83,7 +83,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_category || ""} onChange={e => setDeviceCategory(e.target.value)} - helperText="The category of the device, e.g., 'mobile', 'desktop'" + helperText="The category of the device, e.g., mobile, desktop" /> @@ -95,7 +95,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_language || ""} onChange={e => setDeviceLanguage(e.target.value)} - helperText="The language of the device in ISO 639-1 format, e.g., 'en'" + helperText="The language of the device in ISO 639-1 format, e.g., en" /> @@ -107,7 +107,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_screen_resolution || ""} onChange={e => setDeviceScreenResolution(e.target.value)} - helperText="The resolution of the device, e.g., '1920x1080'." + helperText="The screen resolution of the device, e.g., 1920x1080" /> @@ -119,7 +119,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_operating_system || ""} onChange={e => setDeviceOperatingSystem(e.target.value)} - helperText="The operating system of the device, e.g., 'MacOS', 'Windows'." + helperText="The device's operating system, e.g., MacOS, Windows" /> @@ -131,7 +131,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_operating_system_version || ""} onChange={e => setDeviceOperatingSystemVersion(e.target.value)} - helperText="The version of the device's operating system, e.g., '13.5'." + helperText="The version of the device's operating system, e.g., 13.5" /> @@ -143,7 +143,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_model || ""} onChange={e => setDeviceModel(e.target.value)} - helperText="The model of the device, e.g., 'Pixel 6'." + helperText="The model of the device, e.g., Pixel 6" /> @@ -155,7 +155,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_brand || ""} onChange={e => setDeviceBrand(e.target.value)} - helperText="The brand of the device, e.g., 'Google'." + helperText="The brand of the device, e.g., Google" /> @@ -167,7 +167,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_browser || ""} onChange={e => setDeviceBrowser(e.target.value)} - helperText="The brand or type of browser, e.g., 'Chrome'." + helperText="The brand or type of browser, e.g., Chrome" /> @@ -179,7 +179,7 @@ const DeviceInformation: React.FC = ({ size="small" value={device_browser_version || ""} onChange={e => setDeviceBrowserVersion(e.target.value)} - helperText="The version of the browser, e.g., '136.0.7103.60, 5.0'." + helperText="The browser version, e.g., 136.0.7103.60" /> From 29952eecd4c7cebe388c20d3ffee43a2086d5356 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Fri, 19 Sep 2025 17:38:03 +0000 Subject: [PATCH 23/23] update device description --- src/components/ga4/EventBuilder/DeviceInformation.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/ga4/EventBuilder/DeviceInformation.tsx b/src/components/ga4/EventBuilder/DeviceInformation.tsx index 68df3492..85f77c78 100644 --- a/src/components/ga4/EventBuilder/DeviceInformation.tsx +++ b/src/components/ga4/EventBuilder/DeviceInformation.tsx @@ -187,8 +187,9 @@ const DeviceInformation: React.FC = ({ <> User Agent - Specify a user agent string for Google Analytics to use to derive device information. - This field is ignored if device information is provided. + Provide a custom user agent string. Google Analytics will use this to derive information + about the user's device, operating system, and browser. This field is ignored if device + attributes are provided. @@ -200,7 +201,7 @@ const DeviceInformation: React.FC = ({ size="small" value={user_agent || ""} onChange={e => setUserAgent(e.target.value)} - helperText="The user agent string." + helperText="The user agent string identifying the client" />