Skip to content

Commit ec43cff

Browse files
authored
Apply Validation Logic to gTag (#1693)
* Remove textbox when gtag.js is checkeked * Validate for firebaseAppId OR measurementID * Fix schema validation for app_instance_id vs client_id * Try to remove weirdly formatted error messaging * Item json schema validations for items array
1 parent c8da06f commit ec43cff

9 files changed

Lines changed: 413 additions & 129 deletions

File tree

src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts

Lines changed: 230 additions & 75 deletions
Large diffs are not rendered by default.

src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,55 +18,72 @@ const RESERVED_USER_PROPERTY_NAMES = [
1818

1919
// formatCheckLib provides additional validations for payload not included in
2020
// the schema validations. All checks are consistent with Firebase documentation.
21-
export const formatCheckLib = (payload, firebaseAppId, api_secret) => {
21+
export const formatCheckLib = (payload, instanceId, api_secret, useFirebase) => {
2222
let errors: ValidationMessage[] = []
2323

24-
const appInstanceIdErrors = isValidAppInstanceId(payload)
24+
const appOrClientErrors = isValidAppOrClientId(payload, useFirebase)
2525
const eventNameErrors = isValidEventName(payload)
2626
const userPropertyNameErrors = isValidUserPropertyName(payload)
2727
const currencyErrors = isValidCurrencyType(payload)
2828
const emptyItemsErrors = isItemsEmpty(payload)
2929
const itemsRequiredKeyErrors = itemsHaveRequiredKey(payload)
30-
const firebaseAppIdErrors = isfirebaseAppIdValid(firebaseAppId)
30+
const instanceIdErrors = isInstanceIdValid(instanceId, useFirebase)
3131
const apiSecretErrors = isApiSecretNotNull(api_secret)
3232
const sizeErrors = isTooBig(payload)
3333

3434
return [
3535
...errors,
36-
...appInstanceIdErrors,
36+
...appOrClientErrors,
3737
...eventNameErrors,
3838
...userPropertyNameErrors,
3939
...currencyErrors,
4040
...emptyItemsErrors,
4141
...itemsRequiredKeyErrors,
42-
...firebaseAppIdErrors,
42+
...instanceIdErrors,
4343
...apiSecretErrors,
4444
...sizeErrors,
4545
]
4646
}
4747

48-
const isValidAppInstanceId = (payload) => {
48+
const isValidAppOrClientId = (payload, useFirebase) => {
4949
let errors: ValidationMessage[] = []
5050
const appInstanceId = payload.app_instance_id
51+
const clientId = payload.client_id
5152

52-
if (appInstanceId) {
53-
if (appInstanceId?.length !== 32) {
53+
if (useFirebase) {
54+
if (appInstanceId) {
55+
if (appInstanceId?.length !== 32) {
56+
errors.push({
57+
description: `Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${appInstanceId.length}] digits.`,
58+
validationCode: "value_invalid",
59+
fieldPath: "app_instance_id"
60+
})
61+
}
62+
63+
if (!appInstanceId.match(/^[A-Fa-f0-9]+$/)) {
64+
let nonChars = appInstanceId.split('').filter((letter: string)=> {
65+
return (!/[0-9A-Fa-f]/.test(letter))
66+
})
67+
68+
errors.push({
69+
description: `Measurement app_instance_id contains non hexadecimal character [${nonChars[0]}].`,
70+
validationCode: "value_invalid",
71+
fieldPath: "app_instance_id"
72+
})
73+
}
74+
} else {
5475
errors.push({
55-
description: `Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${appInstanceId.length}] digits.`,
76+
description: "Measurement requires an app_instance_id.",
5677
validationCode: "value_invalid",
5778
fieldPath: "app_instance_id"
5879
})
5980
}
60-
61-
if (!appInstanceId.match(/^[A-Fa-f0-9]+$/)) {
62-
let nonChars = appInstanceId.split('').filter((letter: string)=> {
63-
return (!/[0-9A-Fa-f]/.test(letter))
64-
})
65-
81+
} else {
82+
if (!clientId) {
6683
errors.push({
67-
description: `Measurement app_instance_id contains non hexadecimal character [${nonChars[0]}].`,
84+
description: "Measurement requires a client_id.",
6885
validationCode: "value_invalid",
69-
fieldPath: "app_instance_id"
86+
fieldPath: "client_id"
7087
})
7188
}
7289
}
@@ -182,15 +199,27 @@ const requiredKeysEmpty = (itemsObj) => {
182199
return !(itemsObj.item_id || itemsObj.item_name)
183200
}
184201

185-
const isfirebaseAppIdValid = (firebaseAppId) => {
202+
const isInstanceIdValid = (instanceId, useFirebase) => {
186203
let errors: ValidationMessage[] = []
204+
const firebaseAppId = instanceId?.firebase_app_id
205+
const measurementId = instanceId?.measurement_id
187206

188-
if (firebaseAppId && !firebaseAppId.match(/[0-9]:[0-9]+:[a-zA-Z]+:[a-zA-Z0-9]+$/)) {
189-
errors.push({
190-
description: `${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path`,
191-
validationCode: "value_invalid",
192-
fieldPath: "firebase_app_id"
193-
})
207+
if (useFirebase) {
208+
if (firebaseAppId && !firebaseAppId.match(/[0-9]:[0-9]+:[a-zA-Z]+:[a-zA-Z0-9]+$/)) {
209+
errors.push({
210+
description: `${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path`,
211+
validationCode: "value_invalid",
212+
fieldPath: "firebase_app_id"
213+
})
214+
}
215+
} else {
216+
if (!measurementId) {
217+
errors.push({
218+
description: "Unable to find non-empty parameter [measurement_id] value in request.",
219+
validationCode: "value_invalid",
220+
fieldPath: "measurement_id"
221+
})
222+
}
194223
}
195224

196225
return errors

src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ const API_DOC_EVENT_URL = 'https://developers.google.com/analytics/devguides/col
99
const API_DOC_USER_PROPERTIES = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?hl=en&client_type=firebase'
1010
const API_DOC_SENDING_EVENTS_URL = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?hl=en&client_type=firebase'
1111
const API_DOC_JSON_POST_BODY = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=en&client_type=firebase#payload_post_body'
12+
const API_DOC_GTAG = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#required_parameters'
1213

1314
const BASE_PAYLOAD_ATTRIBUTES = ['app_instance_id', 'api_secret', 'firebase_app_id', 'user_id', 'timestamp_micros', 'user_properties', 'non_personalized_ads']
1415

1516
// formats error messages for clarity; add documentation to each error
16-
export const formatErrorMessages = (errors, payload) => {
17+
export const formatErrorMessages = (errors, payload, useFirebase) => {
1718
const formattedErrors = errors.map(error => {
1819
const { description, fieldPath } = error
1920

@@ -28,6 +29,7 @@ export const formatErrorMessages = (errors, payload) => {
2829
error['description'] = description.slice(0, end_index) + ALPHA_NUMERIC_OVERRIDE
2930

3031
return error
32+
3133
} else if (BASE_PAYLOAD_ATTRIBUTES.includes(fieldPath?.slice(2))) {
3234
error['fieldPath'] = fieldPath.slice(2)
3335

@@ -39,20 +41,22 @@ export const formatErrorMessages = (errors, payload) => {
3941
})
4042

4143
const documentedErrors = formattedErrors.map(error => {
42-
error['documentation'] = addDocumentation(error, payload)
44+
error['documentation'] = addDocumentation(error, payload, useFirebase)
4345
return error
4446
})
4547

4648
return documentedErrors
4749
}
4850

49-
const addDocumentation = (error, payload) => {
51+
const addDocumentation = (error, payload, useFirebase) => {
5052
const { fieldPath, validationCode } = error
5153

5254
if (validationCode === 'max-length-error' || validationCode === 'max-properties-error' || validationCode === 'max-body-size') {
5355
return API_DOC_LIMITATIONS_URL
5456
} else if (fieldPath?.startsWith('#/events/')) {
5557
return API_DOC_EVENT_URL + payload?.events[0]?.name
58+
} else if (!useFirebase && (fieldPath === 'client_id' || fieldPath === 'measurement_id')) {
59+
return API_DOC_GTAG
5660
} else if (BASE_PAYLOAD_ATTRIBUTES.includes(fieldPath)) {
5761
return API_DOC_BASE_PAYLOAD_URL + fieldPath
5862
} else if (fieldPath === '#/user_properties') {
Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import "jest"
2+
import { invalid } from "moment"
3+
import { ValidationStatus } from "../../types"
24
import { Validator } from "../validator"
35
import { baseContentSchema } from "./baseContent"
46

@@ -17,45 +19,129 @@ describe("baseContentSchema", () => {
1719
expect(validator.isValid(validInput)).toEqual(true)
1820
})
1921

20-
test("is not valid when an app_instance_id has dashes", () => {
22+
test("is not valid with an additional property", () => {
2123
const invalidInput = {
22-
'app_instance_id': '0239500a-23af-4ab0-a79c-58c4042ea175',
23-
'events': []
24+
'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740',
25+
'events': [{
26+
'name': 'something',
27+
'params': {}
28+
}],
29+
'additionalProperty': 123
2430
}
2531

2632
let validator = new Validator(baseContentSchema)
2733

2834
expect(validator.isValid(invalidInput)).toEqual(false)
2935
})
3036

31-
test("is not valid when an app_instance_id is not 32 chars", () => {
32-
const invalidInput = {
33-
'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f74',
34-
'events': []
37+
test("validates specific event names", () => {
38+
const validInput = {
39+
'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740',
40+
'events': [{
41+
'name': 'purchase',
42+
'params': {
43+
'transaction_id': '894982',
44+
'value': 89489,
45+
'currency': 'USD',
46+
'items': [
47+
{
48+
'item_name': 'test'
49+
}
50+
]
51+
}
52+
}],
3553
}
3654

3755
let validator = new Validator(baseContentSchema)
3856

39-
expect(validator.isValid(invalidInput)).toEqual(false)
57+
expect(validator.isValid(validInput)).toEqual(true)
4058
})
4159

42-
test("is not valid with an additional property", () => {
43-
const invalidInput = {
60+
test("validates params don't have reserved suffixes", () => {
61+
const validInput = {
4462
'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740',
45-
'events': [],
46-
'additionalProperty': 123
63+
'events': [{
64+
'name': 'purchase',
65+
'params': {
66+
'transaction_id': '894982',
67+
'value': 89489,
68+
'currency': 'USD',
69+
'ga_test': '123',
70+
'items': [
71+
{
72+
'item_name': 'test'
73+
}
74+
]
75+
}
76+
}],
4777
}
4878

4979
let validator = new Validator(baseContentSchema)
5080

51-
expect(validator.isValid(invalidInput)).toEqual(false)
81+
expect(validator.isValid(validInput)).toEqual(false)
82+
})
83+
84+
test("validates required keys are present for certain events", () => {
85+
const validInput = {
86+
'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740',
87+
'events': [{
88+
'name': 'purchase',
89+
'params': {
90+
'transaction_id': '894982',
91+
'currency': 'USD',
92+
'items': [
93+
{
94+
'item_name': 'test'
95+
}
96+
]
97+
}
98+
}],
99+
}
100+
101+
let validator = new Validator(baseContentSchema)
102+
103+
expect(validator.isValid(validInput)).toEqual(false)
52104
})
53105

54-
test("is not valid without app_instance_id", () => {
55-
const invalidInput = {'events': []}
106+
test("does NOT validate empty item aray for named events, because the error message is too complex and this is validated in formatCheckErrors", () => {
107+
const validInput = {
108+
'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740',
109+
'events': [{
110+
'name': 'purchase',
111+
'params': {
112+
'transaction_id': '894982',
113+
'value': 89489,
114+
'currency': 'USD',
115+
'items': [{}]
116+
}
117+
}],
118+
}
56119

57120
let validator = new Validator(baseContentSchema)
58121

59-
expect(validator.isValid(invalidInput)).toEqual(false)
122+
expect(validator.isValid(validInput)).toEqual(true)
123+
})
124+
125+
test("validates that items don't have reserved name keys", () => {
126+
const validInput = {
127+
'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740',
128+
'events': [{
129+
'name': 'purchase',
130+
'params': {
131+
'transaction_id': '894982',
132+
'value': 89489,
133+
'currency': 'USD',
134+
'items': [
135+
{
136+
'ga_test': 'et'
137+
}
138+
]
139+
}
140+
}],
141+
}
142+
143+
let validator = new Validator(baseContentSchema)
144+
145+
expect(validator.isValid(validInput)).toEqual(false)
60146
})
61147
})

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
// Base JSON Body Content Schema.
22

3-
// from google3.corp.gtech.ads.infrastructure.mapps_s2s_event_validator.schemas import events
43
import { userPropertiesSchema } from './userProperties'
54
import { eventsSchema } from './events'
65

76
export const baseContentSchema = {
87
"type": "object",
9-
"required": ["app_instance_id", "events"],
8+
"required": ["events"],
109
"additionalProperties": false,
1110
"properties": {
1211
"app_instance_id": {
1312
"type": "string",
1413
"format": "app_instance_id"
1514
},
15+
"client_id": {
16+
"type": "string",
17+
},
1618
"user_id": {
1719
"type": "string"
1820
},
@@ -25,4 +27,4 @@ export const baseContentSchema = {
2527
},
2628
"events": eventsSchema,
2729
}
28-
}
30+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { buildEvents } from "./schemaBuilder"
2+
import { itemsSchema } from "./eventTypes/items"
23

34
export const eventSchema = {
45
"type": "object",
@@ -12,7 +13,8 @@ export const eventSchema = {
1213
"pattern": "^[A-Za-z][A-Za-z0-9_]*$",
1314
"maxLength": 40
1415
},
15-
"params": {"type": "object"}
16+
"params": {"type": "object"},
17+
"items": itemsSchema
1618
},
1719
"allOf": buildEvents()
1820
}

src/components/ga4/EventBuilder/ValidateEvent/schemas/eventTypes/item.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import { ITEM_FIELDS } from "./fieldDefinitions"
55

66
export const itemSchema = {
77
"type": "object",
8-
"required": [],
98
"patternProperties": {
109
".": {
1110
"maxLength": 100
1211
}
1312
},
1413
"propertyNames": {
15-
"maxLength": 40
14+
"maxLength": 40,
15+
"pattern":
16+
"^(?!ga_|google_|firebase_)[A-Za-z][A-Za-z0-9_]*$",
1617
},
1718
"properties": ITEM_FIELDS,
1819
"anyOf": [{

0 commit comments

Comments
 (0)