Skip to content

Commit 73b88ac

Browse files
feat(EventBuilder): enable sending geographic info (#2230)
* allow screen_view and ad_impression events * allow geo information fields * add support for geo fields * update ip_override description * revert formatting changes * revert ad_impression and screen_view changes * reorder form fields * simplify implementation * continue simplify implementation, cleanup files * add back labels for geo info * additional file cleanup * revert changes for useTextBox persistence fix, moreved to separate PR * add ip_override and user_location to test * update form field descriptions * remove commented out code
1 parent 280874b commit 73b88ac

10 files changed

Lines changed: 345 additions & 87 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React from "react"
2+
import Chip from "@mui/material/Chip";
3+
import Divider from '@mui/material/Divider';
4+
import { styled } from "@mui/material/styles"
5+
import Typography from "@mui/material/Typography"
6+
import TextField from "@mui/material/TextField"
7+
import Grid from "@mui/material/Grid"
8+
9+
import LinkedTextField from "@/components/LinkedTextField"
10+
import ExternalLink from "@/components/ExternalLink"
11+
import { Label } from "./types"
12+
13+
const Root = styled("div")(({ theme }) => ({
14+
marginTop: theme.spacing(3),
15+
}))
16+
17+
interface GeographicInformationProps {
18+
user_location_city: string | undefined
19+
setUserLocationCity: (value: string) => void
20+
user_location_region_id: string | undefined
21+
setUserLocationRegionId: (value: string) => void
22+
user_location_country_id: string | undefined
23+
setUserLocationCountryId: (value: string) => void
24+
user_location_subcontinent_id: string | undefined
25+
setUserLocationSubcontinentId: (value: string) => void
26+
user_location_continent_id: string | undefined
27+
setUserLocationContinentId: (value: string) => void
28+
ip_override: string | undefined
29+
setIpOverride: (value: string) => void
30+
}
31+
32+
const GeographicInformation: React.FC<GeographicInformationProps> = ({
33+
user_location_city,
34+
setUserLocationCity,
35+
user_location_region_id,
36+
setUserLocationRegionId,
37+
user_location_country_id,
38+
setUserLocationCountryId,
39+
user_location_subcontinent_id,
40+
setUserLocationSubcontinentId,
41+
user_location_continent_id,
42+
setUserLocationContinentId,
43+
ip_override,
44+
setIpOverride,
45+
}) => {
46+
return (
47+
<Root>
48+
<Divider><Chip label="GEOGRAPHIC INFORMATION" size="small" /></Divider>
49+
<Typography variant="h6">User Location</Typography>
50+
<Typography>
51+
See the{" "}
52+
<ExternalLink href="https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference#user_location">
53+
documentation
54+
</ExternalLink>{" "}
55+
for more information about user location attributes.
56+
</Typography>
57+
<Grid container spacing={1}>
58+
<Grid item xs={12} sm={6}>
59+
<TextField
60+
fullWidth
61+
label={Label.City}
62+
id={Label.City}
63+
variant="outlined"
64+
size="small"
65+
value={user_location_city || ""}
66+
onChange={e => setUserLocationCity(e.target.value)}
67+
helperText="The city name, e.g., Mountain View"
68+
/>
69+
</Grid>
70+
<Grid item xs={12} sm={6}>
71+
<TextField
72+
fullWidth
73+
label={Label.RegionId}
74+
id={Label.RegionId}
75+
variant="outlined"
76+
size="small"
77+
value={user_location_region_id || ""}
78+
onChange={e => setUserLocationRegionId(e.target.value)}
79+
helperText="The country and subdivision, e.g., US-CA"
80+
/>
81+
</Grid>
82+
<Grid item xs={12} sm={6}>
83+
<TextField
84+
fullWidth
85+
label={Label.CountryId}
86+
id={Label.CountryId}
87+
variant="outlined"
88+
size="small"
89+
value={user_location_country_id || ""}
90+
onChange={e => setUserLocationCountryId(e.target.value)}
91+
helperText="The country code, e.g., US"
92+
/>
93+
</Grid>
94+
<Grid item xs={12} sm={6}>
95+
<TextField
96+
fullWidth
97+
label={Label.ContinentId}
98+
id={Label.ContinentId}
99+
variant="outlined"
100+
size="small"
101+
value={user_location_continent_id || ""}
102+
onChange={e => setUserLocationContinentId(e.target.value)}
103+
helperText="The continent code, e.g., 019"
104+
/>
105+
</Grid>
106+
<Grid item xs={12} sm={6}>
107+
<TextField
108+
fullWidth
109+
label={Label.SubcontinentId}
110+
id={Label.SubcontinentId}
111+
variant="outlined"
112+
size="small"
113+
value={user_location_subcontinent_id || ""}
114+
onChange={e => setUserLocationSubcontinentId(e.target.value)}
115+
helperText="The subcontinent code, e.g., 021"
116+
/>
117+
</Grid>
118+
</Grid>
119+
<Typography variant="h6">IP Override</Typography>
120+
<Typography>
121+
Provide an IP address to derive the user's geographic location. If
122+
both an IP override and user location are provided, user location will
123+
be used.
124+
</Typography>
125+
<LinkedTextField
126+
label={Label.IpOverride}
127+
id={Label.IpOverride}
128+
linkTitle="See ip_override on devsite."
129+
href="https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference#ip_override"
130+
value={ip_override || ""}
131+
onChange={setIpOverride}
132+
helperText="The IP address of the user."
133+
/>
134+
</Root>
135+
)
136+
}
137+
138+
export default GeographicInformation

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ const renderComponent = (props: Partial<ValidateEventProps> = {}) => {
5353
payloadObj: [],
5454
api_secret: "secret123",
5555
clientIds: {},
56+
ip_override: "",
57+
user_location: {
58+
city: "Mountain View",
59+
region_id: "CA",
60+
country_id: "US",
61+
subcontinent_id: "021",
62+
continent_id: "019"
63+
}
5664
}
5765

5866
return render(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import PrettyJson from "@/components/PrettyJson"
2929
import usePayload from "./usePayload"
3030
import { ValidationMessage } from "../types"
3131
import Spinner from "@/components/Spinner"
32-
import { EventCtx, Label } from ".."
32+
import { EventCtx } from ".."
33+
import { Label } from "../types"
3334
import { Box, Card } from "@mui/material"
3435
import { green, red } from "@mui/material/colors"
3536
import WithHelpText from "@/components/WithHelpText"

src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,16 @@ describe("baseContentSchema", () => {
142142

143143
expect(validator.isValid(validInput)).toEqual(false)
144144
})
145+
146+
describe("with ip_override", () => {
147+
test("is valid with a valid IPv4 address", () => {
148+
const validInput = {
149+
events: [{ name: "something", params: {} }],
150+
ip_override: "127.0.0.1",
151+
}
152+
const validator = new Validator(baseContentSchema)
153+
expect(validator.isValid(validInput)).toEqual(true)
154+
})
155+
})
156+
145157
})

src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,34 @@
22

33
import { userPropertiesSchema } from './userProperties'
44
import { eventsSchema } from './events'
5+
import { userLocationSchema } from "./userLocation"
56

67
export const baseContentSchema = {
7-
"type": "object",
8-
"required": ["events"],
9-
"additionalProperties": false,
10-
"properties": {
11-
"app_instance_id": {
12-
"type": "string",
13-
"format": "app_instance_id"
14-
},
15-
"client_id": {
16-
"type": "string",
17-
},
18-
"user_id": {
19-
"type": "string"
20-
},
21-
"timestamp_micros": {
22-
// "type": "number"
23-
},
24-
"user_properties": userPropertiesSchema,
25-
"non_personalized_ads": {
26-
"type": "boolean"
27-
},
28-
"events": eventsSchema,
29-
}
8+
type: "object",
9+
required: ["events"],
10+
additionalProperties: false,
11+
properties: {
12+
app_instance_id: {
13+
type: "string",
14+
format: "app_instance_id",
15+
},
16+
client_id: {
17+
type: "string",
18+
},
19+
user_id: {
20+
type: "string",
21+
},
22+
timestamp_micros: {
23+
// "type": "number"
24+
},
25+
user_properties: userPropertiesSchema,
26+
non_personalized_ads: {
27+
type: "boolean",
28+
},
29+
events: eventsSchema,
30+
user_location: userLocationSchema,
31+
ip_override: {
32+
type: "string",
33+
},
34+
},
3035
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// User location schema
2+
3+
export const userLocationSchema = {
4+
type: "object",
5+
additionalProperties: false,
6+
properties: {
7+
city: {
8+
type: "string",
9+
},
10+
region_id: {
11+
type: "string",
12+
},
13+
country_id: {
14+
type: "string",
15+
},
16+
subcontinent_id: {
17+
type: "string",
18+
},
19+
continent_id: {
20+
type: "string",
21+
},
22+
},
23+
}

src/components/ga4/EventBuilder/ValidateEvent/usePayload.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ const usePayload = (): {} => {
7171
clientIds,
7272
type,
7373
useTextBox,
74-
payloadObj
74+
payloadObj,
75+
ip_override,
76+
user_location,
7577
} = useContext(EventCtx)!
7678

7779
const eventName = useMemo(() => {
@@ -93,22 +95,34 @@ const usePayload = (): {} => {
9395
[items]
9496
)
9597

96-
const params = useMemo(() => parameters.reduce(objectify, itemsParameter), [
97-
parameters,
98-
itemsParameter,
99-
])
98+
const params = useMemo(
99+
() => parameters.reduce(objectify, itemsParameter),
100+
[parameters, itemsParameter]
101+
)
100102

101103
const user_properties = useMemo(
102104
() => userProperties.reduce(objectifyUserProperties, {}),
103105
[userProperties]
104106
)
105107

108+
const user_location_info = useMemo(() => {
109+
if (user_location === undefined) {
110+
return undefined
111+
}
112+
const cleaned_location = removeUndefined(user_location)
113+
if (Object.keys(cleaned_location).length === 0) {
114+
return undefined
115+
}
116+
return cleaned_location
117+
}, [user_location])
118+
106119
let payload = useMemo(() => {
107120
return {
108121
...removeUndefined(clientIds),
109122
...removeUndefined({ timestamp_micros }),
110123
...removeUndefined({ non_personalized_ads }),
111124
...removeUndefined(removeEmptyObject({ user_properties })),
125+
...removeUndefined({ ip_override, user_location: user_location_info }),
112126
events: [
113127
{ name: eventName, ...(parameters.length > 0 ? { params } : {}) },
114128
],
@@ -121,6 +135,8 @@ const usePayload = (): {} => {
121135
params,
122136
timestamp_micros,
123137
user_properties,
138+
ip_override,
139+
user_location_info,
124140
])
125141

126142
if (useTextBox) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import * as renderer from "@testing-library/react"
1818
import "@testing-library/jest-dom"
1919

2020
import { withProviders } from "@/test-utils"
21-
import Sut, { Label } from "./index"
21+
import Sut from "./index"
22+
import { Label } from "./types"
2223
import userEvent from "@testing-library/user-event"
2324
import { within } from "@testing-library/react"
2425

0 commit comments

Comments
 (0)