Skip to content

Commit

Permalink
Add initial firebase measurement protocol schema validations (#1597)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
marycodes2 authored Apr 19, 2023
1 parent 202c46d commit 77e25a9
Show file tree
Hide file tree
Showing 30 changed files with 1,387 additions and 25 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
"gatsby-transformer-sharp": "^3.4.0",
"immutable": "^4.0.0-rc.12",
"js-base64": "^3.6.1",
"json-schema-library": "^7.4.7",
"load-script": "^1.0.0",
"moment": "^2.29.1",
"object-sizeof": "^2.6.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
Expand Down
1 change: 1 addition & 0 deletions src/components/ga4/EventBuilder/Parameter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const Parameter: React.FC<Props> = ({
const inputs = (
<section className={classes.parameter}>
<TextField
id={`#/events/0/params/${name}`}
variant="outlined"
size="small"
value={name}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import "jest"
import { formatCheckLib } from "./formatCheckLib"

describe("formatCheckLib", () => {
describe("returns appInstanceIdErrors", () => {
test("does not return an error when app_instance_id is 32 alpha-numeric chars", () => {
const payload = {app_instance_id: "12345678901234567890123456789012"}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("returns an error when appInstanceId is not 32 chars", () => {
const appInstanceId = "123456789012345678901234567890123"
const payload = {app_instance_id: appInstanceId}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
`Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${ appInstanceId.length }] digits.`
)
})

test("returns an error when appInstanceId contains non-alphanumeric char", () => {
const appInstanceId = "1234567890123456789012345678901g"
const payload = {app_instance_id: appInstanceId}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
`Measurement app_instance_id contains non hexadecimal character [g].`,
)
})
})

describe("returns invalidEventName errors", () => {
test("does not return an error for a valid event name", () => {
const payload = {events: [{name: 'add_payment_info'}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("returns an error when event's name is a reserved name", () => {
const payload = {events: [{name: 'ad_click'}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
"ad_click is a reserved event name"
)
})
})

describe("returns invalidUserPropertyName errors", () => {
test("does not return an error for a valid user property name", () => {
const payload = {user_properties: {'test': 'test'}}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("returns an error when event's name is a reserved name", () => {
const payload = {user_properties: {'first_open_time': 'test'}}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
"user_property: 'first_open_time' is a reserved user property name"
)
})
})

describe("returns invalidCurrencyType errors", () => {
test("does not return an error for a valid currency type", () => {
const payload = {events: [{params: {currency: 'USD'}}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("returns an error for an invalid currency type", () => {
const payload = {events: [{params: {currency: 'USDD'}}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
"currency: USDD must be a valid uppercase 3-letter ISO 4217 format"
)
})
})

describe("validates items array", () => {
test("does not return an error if items array is valid", () => {
const payload = {events: [{params: {items: [{'item_id': 1234}]}}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("returns an error when items does not have either item_id or item_name", () => {
const payload = {events: [{params: {items: [{'item_namee': 'test'}]}}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
"'items' object must contain one of the following keys: 'item_id' or 'item_name'"
)
})

test("validates empty items array when event requires items", () => {
const payload = {events: [{params: {name: 'purchase', items: []}}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
"'items' should not be empty; One of 'item_id' or 'item_name' is a required key"
)
})

test("does not validate empty items array when event doesn't require items", () => {
const payload = {events: [{params: {name: 'random', items: []}}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("does not validate empty items array when event does not have a name", () => {
const payload = {events: [{params: {items: []}}]}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("returns an error when item_id and item_name keys have empty values", () => {
const payload = {
events: [
{
params: {
items: [
{
'item_name': '',
'item_id': ''
}
]
}
}
]
}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
"'items' object must contain one of the following keys: 'item_id' or 'item_name'"
)
})
})

describe("validates firebase_app_id", () => {
test("does not return an error if firebase_app_id is valid", () => {
const payload = {}
const firebaseAppId = '1:1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors).toEqual([])
})

test("returns an error when firebase_app_id is invalid", () => {
const payload = {}
const firebaseAppId = '1233455666:android:abcdefgh'

let errors = formatCheckLib(payload, firebaseAppId)

expect(errors[0].description).toEqual(
`${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path`
)
})
})
})
Loading

0 comments on commit 77e25a9

Please sign in to comment.