Skip to content

Commit 77e25a9

Browse files
authored
Add initial firebase measurement protocol schema validations (#1597)
* Add Schemas and validation class; begin tests * Finish event schema spec * Update state with validationMessages with messages from json validator * ValidationMessages added to event builder frontend * Add custom checks on payload attributes * Remove duplicate error messages * Only validate firebase payloads; aadd tests to schema validations * cleanup * Require login for event builder page * Fix formatting warning * Ensure userProperties exists before adding error * Add specs, fix issues, begin formatting errors * Add documentation to each error * Add focus button to most errors * Fix small bugs, fix failing specs * Add additional check for params and items of events * Respond to code review * Fix failing spec; fix focus button on schema returned errors * Removing params and items list checks -- not sure if they will prevent payload submission * Don't validate empty items for events that don't require items * fix warnings 1 * fix warnings 2
1 parent 202c46d commit 77e25a9

30 files changed

Lines changed: 1387 additions & 25 deletions

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
"gatsby-transformer-sharp": "^3.4.0",
3434
"immutable": "^4.0.0-rc.12",
3535
"js-base64": "^3.6.1",
36+
"json-schema-library": "^7.4.7",
3637
"load-script": "^1.0.0",
3738
"moment": "^2.29.1",
39+
"object-sizeof": "^2.6.1",
3840
"react": "^17.0.2",
3941
"react-dom": "^17.0.2",
4042
"react-error-boundary": "^3.1.3",

src/components/ga4/EventBuilder/Parameter.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const Parameter: React.FC<Props> = ({
4444
const inputs = (
4545
<section className={classes.parameter}>
4646
<TextField
47+
id={`#/events/0/params/${name}`}
4748
variant="outlined"
4849
size="small"
4950
value={name}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import "jest"
2+
import { formatCheckLib } from "./formatCheckLib"
3+
4+
describe("formatCheckLib", () => {
5+
describe("returns appInstanceIdErrors", () => {
6+
test("does not return an error when app_instance_id is 32 alpha-numeric chars", () => {
7+
const payload = {app_instance_id: "12345678901234567890123456789012"}
8+
const firebaseAppId = '1:1233455666:android:abcdefgh'
9+
10+
let errors = formatCheckLib(payload, firebaseAppId)
11+
12+
expect(errors).toEqual([])
13+
})
14+
15+
test("returns an error when appInstanceId is not 32 chars", () => {
16+
const appInstanceId = "123456789012345678901234567890123"
17+
const payload = {app_instance_id: appInstanceId}
18+
const firebaseAppId = '1:1233455666:android:abcdefgh'
19+
20+
let errors = formatCheckLib(payload, firebaseAppId)
21+
22+
expect(errors[0].description).toEqual(
23+
`Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${ appInstanceId.length }] digits.`
24+
)
25+
})
26+
27+
test("returns an error when appInstanceId contains non-alphanumeric char", () => {
28+
const appInstanceId = "1234567890123456789012345678901g"
29+
const payload = {app_instance_id: appInstanceId}
30+
const firebaseAppId = '1:1233455666:android:abcdefgh'
31+
32+
let errors = formatCheckLib(payload, firebaseAppId)
33+
34+
expect(errors[0].description).toEqual(
35+
`Measurement app_instance_id contains non hexadecimal character [g].`,
36+
)
37+
})
38+
})
39+
40+
describe("returns invalidEventName errors", () => {
41+
test("does not return an error for a valid event name", () => {
42+
const payload = {events: [{name: 'add_payment_info'}]}
43+
const firebaseAppId = '1:1233455666:android:abcdefgh'
44+
45+
let errors = formatCheckLib(payload, firebaseAppId)
46+
47+
expect(errors).toEqual([])
48+
})
49+
50+
test("returns an error when event's name is a reserved name", () => {
51+
const payload = {events: [{name: 'ad_click'}]}
52+
const firebaseAppId = '1:1233455666:android:abcdefgh'
53+
54+
let errors = formatCheckLib(payload, firebaseAppId)
55+
56+
expect(errors[0].description).toEqual(
57+
"ad_click is a reserved event name"
58+
)
59+
})
60+
})
61+
62+
describe("returns invalidUserPropertyName errors", () => {
63+
test("does not return an error for a valid user property name", () => {
64+
const payload = {user_properties: {'test': 'test'}}
65+
const firebaseAppId = '1:1233455666:android:abcdefgh'
66+
67+
let errors = formatCheckLib(payload, firebaseAppId)
68+
69+
expect(errors).toEqual([])
70+
})
71+
72+
test("returns an error when event's name is a reserved name", () => {
73+
const payload = {user_properties: {'first_open_time': 'test'}}
74+
const firebaseAppId = '1:1233455666:android:abcdefgh'
75+
76+
let errors = formatCheckLib(payload, firebaseAppId)
77+
78+
expect(errors[0].description).toEqual(
79+
"user_property: 'first_open_time' is a reserved user property name"
80+
)
81+
})
82+
})
83+
84+
describe("returns invalidCurrencyType errors", () => {
85+
test("does not return an error for a valid currency type", () => {
86+
const payload = {events: [{params: {currency: 'USD'}}]}
87+
const firebaseAppId = '1:1233455666:android:abcdefgh'
88+
89+
let errors = formatCheckLib(payload, firebaseAppId)
90+
91+
expect(errors).toEqual([])
92+
})
93+
94+
test("returns an error for an invalid currency type", () => {
95+
const payload = {events: [{params: {currency: 'USDD'}}]}
96+
const firebaseAppId = '1:1233455666:android:abcdefgh'
97+
98+
let errors = formatCheckLib(payload, firebaseAppId)
99+
100+
expect(errors[0].description).toEqual(
101+
"currency: USDD must be a valid uppercase 3-letter ISO 4217 format"
102+
)
103+
})
104+
})
105+
106+
describe("validates items array", () => {
107+
test("does not return an error if items array is valid", () => {
108+
const payload = {events: [{params: {items: [{'item_id': 1234}]}}]}
109+
const firebaseAppId = '1:1233455666:android:abcdefgh'
110+
111+
let errors = formatCheckLib(payload, firebaseAppId)
112+
113+
expect(errors).toEqual([])
114+
})
115+
116+
test("returns an error when items does not have either item_id or item_name", () => {
117+
const payload = {events: [{params: {items: [{'item_namee': 'test'}]}}]}
118+
const firebaseAppId = '1:1233455666:android:abcdefgh'
119+
120+
let errors = formatCheckLib(payload, firebaseAppId)
121+
122+
expect(errors[0].description).toEqual(
123+
"'items' object must contain one of the following keys: 'item_id' or 'item_name'"
124+
)
125+
})
126+
127+
test("validates empty items array when event requires items", () => {
128+
const payload = {events: [{params: {name: 'purchase', items: []}}]}
129+
const firebaseAppId = '1:1233455666:android:abcdefgh'
130+
131+
let errors = formatCheckLib(payload, firebaseAppId)
132+
133+
expect(errors[0].description).toEqual(
134+
"'items' should not be empty; One of 'item_id' or 'item_name' is a required key"
135+
)
136+
})
137+
138+
test("does not validate empty items array when event doesn't require items", () => {
139+
const payload = {events: [{params: {name: 'random', items: []}}]}
140+
const firebaseAppId = '1:1233455666:android:abcdefgh'
141+
142+
let errors = formatCheckLib(payload, firebaseAppId)
143+
144+
expect(errors).toEqual([])
145+
})
146+
147+
test("does not validate empty items array when event does not have a name", () => {
148+
const payload = {events: [{params: {items: []}}]}
149+
const firebaseAppId = '1:1233455666:android:abcdefgh'
150+
151+
let errors = formatCheckLib(payload, firebaseAppId)
152+
153+
expect(errors).toEqual([])
154+
})
155+
156+
test("returns an error when item_id and item_name keys have empty values", () => {
157+
const payload = {
158+
events: [
159+
{
160+
params: {
161+
items: [
162+
{
163+
'item_name': '',
164+
'item_id': ''
165+
}
166+
]
167+
}
168+
}
169+
]
170+
}
171+
const firebaseAppId = '1:1233455666:android:abcdefgh'
172+
173+
let errors = formatCheckLib(payload, firebaseAppId)
174+
175+
expect(errors[0].description).toEqual(
176+
"'items' object must contain one of the following keys: 'item_id' or 'item_name'"
177+
)
178+
})
179+
})
180+
181+
describe("validates firebase_app_id", () => {
182+
test("does not return an error if firebase_app_id is valid", () => {
183+
const payload = {}
184+
const firebaseAppId = '1:1233455666:android:abcdefgh'
185+
186+
let errors = formatCheckLib(payload, firebaseAppId)
187+
188+
expect(errors).toEqual([])
189+
})
190+
191+
test("returns an error when firebase_app_id is invalid", () => {
192+
const payload = {}
193+
const firebaseAppId = '1233455666:android:abcdefgh'
194+
195+
let errors = formatCheckLib(payload, firebaseAppId)
196+
197+
expect(errors[0].description).toEqual(
198+
`${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path`
199+
)
200+
})
201+
})
202+
})

0 commit comments

Comments
 (0)