From c1ea1743e3e0908d611c7af0581c88eb760005c3 Mon Sep 17 00:00:00 2001 From: Mary Beliveau <39474777+marycodes2@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:26:56 -0700 Subject: [PATCH 1/5] Remove textbox when gtag.js is checkeked --- src/components/ga4/EventBuilder/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 24880fff..ed520c36 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -252,6 +252,10 @@ const EventBuilder: React.FC = () => { checked={useFirebase} onChange={e => { setUseFirebase(e.target.checked) + + if (!e.target.checked) { + setUseTextBox(false) + } }} name="use firebase" color="primary" From 8e190bddae521923abefd146ac786cb34c704832 Mon Sep 17 00:00:00 2001 From: Mary Beliveau <39474777+marycodes2@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:52:47 -0700 Subject: [PATCH 2/5] Validate for firebaseAppId OR measurementID --- .../handlers/formatCheckLib.spec.ts | 135 +++++++++++++----- .../ValidateEvent/handlers/formatCheckLib.ts | 32 +++-- .../ValidateEvent/schemas/baseContent.ts | 1 - .../ValidateEvent/useValidateEvent.ts | 7 +- 4 files changed, 129 insertions(+), 46 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts index 0e298d50..b8bf959c 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts @@ -6,9 +6,10 @@ describe("formatCheckLib", () => { 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' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -17,9 +18,10 @@ describe("formatCheckLib", () => { const appInstanceId = "123456789012345678901234567890123" const payload = {app_instance_id: appInstanceId} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( `Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${ appInstanceId.length }] digits.` @@ -30,9 +32,10 @@ describe("formatCheckLib", () => { const appInstanceId = "1234567890123456789012345678901g" const payload = {app_instance_id: appInstanceId} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( `Measurement app_instance_id contains non hexadecimal character [g].`, @@ -44,9 +47,10 @@ describe("formatCheckLib", () => { test("does not return an error for a valid event name", () => { const payload = {events: [{name: 'add_payment_info'}]} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -54,9 +58,10 @@ describe("formatCheckLib", () => { test("returns an error when event's name is a reserved name", () => { const payload = {events: [{name: 'ad_click'}]} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( "ad_click is a reserved event name" @@ -68,9 +73,10 @@ describe("formatCheckLib", () => { test("does not return an error for a valid user property name", () => { const payload = {user_properties: {'test': 'test'}} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -78,9 +84,10 @@ describe("formatCheckLib", () => { 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' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( "user_property: 'first_open_time' is a reserved user property name" @@ -92,9 +99,10 @@ describe("formatCheckLib", () => { test("does not return an error for a valid currency type", () => { const payload = {events: [{params: {currency: 'USD'}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -102,9 +110,10 @@ describe("formatCheckLib", () => { test("returns an error for an invalid currency type", () => { const payload = {events: [{params: {currency: 'USDD'}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( "currency: USDD must be a valid uppercase 3-letter ISO 4217 format" @@ -116,9 +125,10 @@ describe("formatCheckLib", () => { 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' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -126,9 +136,10 @@ describe("formatCheckLib", () => { 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' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( "'items' object must contain one of the following keys: 'item_id' or 'item_name'" @@ -138,9 +149,10 @@ describe("formatCheckLib", () => { test("validates empty items array when event requires items", () => { const payload = {events: [{params: {name: 'purchase', items: []}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( "'items' should not be empty; One of 'item_id' or 'item_name' is a required key" @@ -150,9 +162,10 @@ describe("formatCheckLib", () => { 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' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -160,9 +173,10 @@ describe("formatCheckLib", () => { 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' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -183,9 +197,10 @@ describe("formatCheckLib", () => { ] } const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( "'items' object must contain one of the following keys: 'item_id' or 'item_name'" @@ -193,27 +208,81 @@ describe("formatCheckLib", () => { }) }) - 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' - const api_secret = '123' + describe("validates instance_id", () => { + describe("when useFirebase is true", () => { + test("does not return an error if firebase_app_id is valid", () => { + const payload = {} + const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) - expect(errors).toEqual([]) + expect(errors).toEqual([]) + }) + + test("returns an error when firebase_app_id is invalid", () => { + const payload = {} + const firebaseAppId = '1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, true) + + expect(errors[0].description).toEqual( + `${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path` + ) + }) + + test("does not validate measurement_id", () => { + const payload = {} + const firebaseAppId = '1:1233455666:android:abcdefgh' + const measurementId = '' + const instanceId = {firebase_app_id: firebaseAppId, measurement_id: measurementId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, true) + + expect(errors).toEqual([]) + }) }) - test("returns an error when firebase_app_id is invalid", () => { - const payload = {} - const firebaseAppId = '1233455666:android:abcdefgh' - const api_secret = '123' + describe("when useFirebase is false", () => { + test("does not return an error if measurement_id is not null", () => { + const payload = {} + const measurementId = 'test' + const instanceId = {measurement_id: measurementId} + const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, false) - expect(errors[0].description).toEqual( - `${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path` - ) + expect(errors).toEqual([]) + }) + + test("returns an error when meausrement_id is null", () => { + const payload = {} + const measurementId = '' + const instanceId = {measurement_id: measurementId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, false) + + expect(errors[0].description).toEqual( + "Unable to find non-empty parameter [measurement_id] value in request." + ) + }) + + test("does not validate firebase_app_id", () => { + const payload = {} + const firebaseAppId = '1233455666:android:abcdefgh' + const measurementId = 'test' + const instanceId = {firebase_app_id: firebaseAppId, measurement_id: measurementId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, false) + + expect(errors).toEqual([]) + }) }) }) @@ -221,9 +290,10 @@ describe("formatCheckLib", () => { test("does not return an error api_secret is not null", () => { const payload = {} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) @@ -231,9 +301,10 @@ describe("formatCheckLib", () => { test("returns an error when api_secret is null", () => { const payload = {} const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors[0].description).toEqual( "Unable to find non-empty parameter [api_secret] value in request." diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts index 6400f187..d5c8801d 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts @@ -18,7 +18,7 @@ const RESERVED_USER_PROPERTY_NAMES = [ // formatCheckLib provides additional validations for payload not included in // the schema validations. All checks are consistent with Firebase documentation. -export const formatCheckLib = (payload, firebaseAppId, api_secret) => { +export const formatCheckLib = (payload, instanceId, api_secret, useFirebase) => { let errors: ValidationMessage[] = [] const appInstanceIdErrors = isValidAppInstanceId(payload) @@ -27,7 +27,7 @@ export const formatCheckLib = (payload, firebaseAppId, api_secret) => { const currencyErrors = isValidCurrencyType(payload) const emptyItemsErrors = isItemsEmpty(payload) const itemsRequiredKeyErrors = itemsHaveRequiredKey(payload) - const firebaseAppIdErrors = isfirebaseAppIdValid(firebaseAppId) + const instanceIdErrors = isInstanceIdValid(instanceId, useFirebase) const apiSecretErrors = isApiSecretNotNull(api_secret) const sizeErrors = isTooBig(payload) @@ -39,7 +39,7 @@ export const formatCheckLib = (payload, firebaseAppId, api_secret) => { ...currencyErrors, ...emptyItemsErrors, ...itemsRequiredKeyErrors, - ...firebaseAppIdErrors, + ...instanceIdErrors, ...apiSecretErrors, ...sizeErrors, ] @@ -182,15 +182,27 @@ const requiredKeysEmpty = (itemsObj) => { return !(itemsObj.item_id || itemsObj.item_name) } -const isfirebaseAppIdValid = (firebaseAppId) => { +const isInstanceIdValid = (instanceId, useFirebase) => { let errors: ValidationMessage[] = [] + const firebaseAppId = instanceId?.firebase_app_id + const measurementId = instanceId?.measurement_id - if (firebaseAppId && !firebaseAppId.match(/[0-9]:[0-9]+:[a-zA-Z]+:[a-zA-Z0-9]+$/)) { - errors.push({ - description: `${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path`, - validationCode: "value_invalid", - fieldPath: "firebase_app_id" - }) + if (useFirebase) { + if (firebaseAppId && !firebaseAppId.match(/[0-9]:[0-9]+:[a-zA-Z]+:[a-zA-Z0-9]+$/)) { + errors.push({ + description: `${firebaseAppId} does not follow firebase_app_id pattern of X:XX:XX:XX at path`, + validationCode: "value_invalid", + fieldPath: "firebase_app_id" + }) + } + } else { + if (!measurementId) { + errors.push({ + description: "Unable to find non-empty parameter [measurement_id] value in request.", + validationCode: "value_invalid", + fieldPath: "measurement_id" + }) + } } return errors diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts index 6a98b5f2..dc04a633 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts @@ -1,6 +1,5 @@ // Base JSON Body Content Schema. -// from google3.corp.gtech.ads.infrastructure.mapps_s2s_event_validator.schemas import events import { userPropertiesSchema } from './userProperties' import { eventsSchema } from './events' diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts b/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts index 48ae3f92..9d425adc 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts @@ -131,8 +131,9 @@ const useValidateEvent = (): Requestable< let validator = new Validator(baseContentSchema) let formatCheckErrors: ValidationMessage[] | [] = formatCheckLib( payload, - instanceId?.firebase_app_id, - api_secret + instanceId, + api_secret, + useFirebase ) if (!validator.isValid(payload) || formatCheckErrors) { @@ -158,7 +159,7 @@ const useValidateEvent = (): Requestable< setValidationMessages([]) if (!useTextBox || Object.keys(payload).length !== 0) { - let validatorErrors = useFirebase ? validatePayloadAttributes(payload) : [] + let validatorErrors = validatePayloadAttributes(payload) validateHit(payload, instanceId, api_secret) .then(messages => { From fc351114db3efd3331854771967d9be64f8b8933 Mon Sep 17 00:00:00 2001 From: Mary Beliveau <39474777+marycodes2@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:21:55 -0700 Subject: [PATCH 3/5] Fix schema validation for app_instance_id vs client_id --- .../ValidateEvent/schemas/baseContent.ts | 93 +++++++++++++++---- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts index dc04a633..e4ae2e34 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts @@ -3,25 +3,82 @@ import { userPropertiesSchema } from './userProperties' import { eventsSchema } from './events' +// export const baseContentSchema = { +// "type": "object", +// "required": ["app_instance_id", "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, +// } +// } + export const baseContentSchema = { "type": "object", - "required": ["app_instance_id", "events"], - "additionalProperties": false, - "properties": { - "app_instance_id": { - "type": "string", - "format": "app_instance_id" - }, - "user_id": { - "type": "string" - }, - "timestamp_micros": { - // "type": "number" + "oneOf": [ + { + "required": ["app_instance_id", "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_properties": userPropertiesSchema, - "non_personalized_ads": { - "type": "boolean" + { + "required": ["client_id", "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, + } }, - "events": eventsSchema, - } -} + ], +} \ No newline at end of file From 94934bb33fab0cd787da2886624468521774fea3 Mon Sep 17 00:00:00 2001 From: Mary Beliveau <39474777+marycodes2@users.noreply.github.com> Date: Thu, 27 Jul 2023 18:02:36 -0700 Subject: [PATCH 4/5] Try to remove weirdly formatted error messaging --- .../ValidateEvent/handlers/responseUtil.ts | 19 ++++++++++++++++++- .../ValidateEvent/useValidateEvent.ts | 4 ++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts index e233d339..ee9722ff 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts @@ -2,6 +2,10 @@ const ALPHA_NUMERIC_NAME = "does not match '^(?!ga_|google_|firebase_)[A-Za-z][A const ALPHA_NUMERIC_OVERRIDE = " may only contain alpha-numeric characters and underscores,start with an alphabetic character, and cannot contain google_, ga_, firebase_" const CUSTOM_PARAMS_NAME = "can have at most [10] custom params." const ITEM_INVALID_KEY_OVERRIDE = "Item array has invalid key" +const ONEOF_SCHEMA_ERROR = "does not match any given oneof schema" +const ONEOF_SCHEMA_ERROR_OVERRIDE_CLIENT_ID = "Measurement requires a client_id." +const ONEOF_SCHEMA_ERROR_OVERRIDE_APP_INSTANCE_ID = "Measurement requires an app_instance_id." + const API_DOC_LIMITATIONS_URL = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?hl=en&client_type=firebase#limitations' const API_DOC_BASE_PAYLOAD_URL = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference#' @@ -13,7 +17,7 @@ const API_DOC_JSON_POST_BODY = 'https://developers.google.com/analytics/devguide const BASE_PAYLOAD_ATTRIBUTES = ['app_instance_id', 'api_secret', 'firebase_app_id', 'user_id', 'timestamp_micros', 'user_properties', 'non_personalized_ads'] // formats error messages for clarity; add documentation to each error -export const formatErrorMessages = (errors, payload) => { +export const formatErrorMessages = (errors, payload, useFirebase) => { const formattedErrors = errors.map(error => { const { description, fieldPath } = error @@ -27,6 +31,19 @@ export const formatErrorMessages = (errors, payload) => { let end_index = description.indexOf(ALPHA_NUMERIC_NAME); error['description'] = description.slice(0, end_index) + ALPHA_NUMERIC_OVERRIDE + return error + + } else if (description.endsWith(ONEOF_SCHEMA_ERROR)) { + if (useFirebase) { + error['description'] = ONEOF_SCHEMA_ERROR_OVERRIDE_APP_INSTANCE_ID + error['fieldPath'] = 'app_instance_id' + } else { + error['description'] = ONEOF_SCHEMA_ERROR_OVERRIDE_CLIENT_ID + error['fieldPath'] = 'client_id' + } + + error['validationCode'] = 'VALUE_REQUIRED' + return error } else if (BASE_PAYLOAD_ATTRIBUTES.includes(fieldPath?.slice(2))) { error['fieldPath'] = fieldPath.slice(2) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts b/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts index 9d425adc..b33d785c 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts @@ -172,14 +172,14 @@ const useValidateEvent = (): Requestable< ? useFirebase : true ) - + apiValidationErrors.forEach(err => { if (!validatorErrors.map(e => e.description).includes(err.description)) { validatorErrors.push(err) } }) - validatorErrors = formatErrorMessages(validatorErrors, payload) + validatorErrors = formatErrorMessages(validatorErrors, payload, useFirebase) setValidationMessages(validatorErrors) setStatus(RequestStatus.Failed) From 5b32319791924dc0471bf7e8b24a8997dd29074b Mon Sep 17 00:00:00 2001 From: Mary Beliveau <39474777+marycodes2@users.noreply.github.com> Date: Thu, 27 Jul 2023 20:06:04 -0700 Subject: [PATCH 5/5] Item json schema validations for items array --- .../handlers/formatCheckLib.spec.ts | 180 +++++++++++++----- .../ValidateEvent/handlers/formatCheckLib.ts | 45 +++-- .../ValidateEvent/handlers/responseUtil.ts | 23 +-- .../ValidateEvent/schemas/baseContent.spec.ts | 118 ++++++++++-- .../ValidateEvent/schemas/baseContent.ts | 94 ++------- .../ValidateEvent/schemas/event.ts | 4 +- .../ValidateEvent/schemas/eventTypes/item.ts | 5 +- 7 files changed, 296 insertions(+), 173 deletions(-) diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts index b8bf959c..cfc367cf 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts @@ -2,50 +2,133 @@ 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' - const instanceId = {firebase_app_id: firebaseAppId} - const api_secret = '123' + describe("returns appOrClientErrors", () => { + describe("when useFirebase is true", () => { + 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' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' - let errors = formatCheckLib(payload, instanceId, api_secret, true) + let errors = formatCheckLib(payload, instanceId, api_secret, true) - expect(errors).toEqual([]) - }) + 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' - const instanceId = {firebase_app_id: firebaseAppId} - const api_secret = '123' + 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' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' - let errors = formatCheckLib(payload, instanceId, api_secret, true) + let errors = formatCheckLib(payload, instanceId, api_secret, true) - expect(errors[0].description).toEqual( - `Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${ appInstanceId.length }] digits.` - ) + 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' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, true) + + expect(errors[0].description).toEqual( + `Measurement app_instance_id contains non hexadecimal character [g].`, + ) + }) + + test("returns an error when appInstanceId is null", () => { + const appInstanceId = "" + const payload = {app_instance_id: appInstanceId} + const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, true) + + expect(errors[0].description).toEqual( + "Measurement requires an app_instance_id.", + ) + }) + + test("returns an error when appInstanceId not present in the payload", () => { + const payload = {} + const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, true) + + expect(errors[0].description).toEqual( + "Measurement requires an app_instance_id.", + ) + }) }) - 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' - const instanceId = {firebase_app_id: firebaseAppId} - const api_secret = '123' + describe("when useFirebase is false", () => { + test("does not return an error when client_id is present", () => { + const payload = {client_id: "12345678901234567890123456789012"} + const measurementId = 'test' + const instanceId = {measurement_id: measurementId} + const api_secret = '123' - let errors = formatCheckLib(payload, instanceId, api_secret, true) + let errors = formatCheckLib(payload, instanceId, api_secret, false) - expect(errors[0].description).toEqual( - `Measurement app_instance_id contains non hexadecimal character [g].`, - ) + expect(errors).toEqual([]) + }) + + test("returns an error when client_id is not present", () => { + const payload = {} + const measurementId = 'test' + const instanceId = {measurement_id: measurementId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, false) + + expect(errors[0].description).toEqual( + "Measurement requires a client_id.", + ) + }) + + 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' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, true) + + expect(errors[0].description).toEqual( + `Measurement app_instance_id contains non hexadecimal character [g].`, + ) + }) + + test("returns an error when appInstanceId is null", () => { + const appInstanceId = "" + const payload = {app_instance_id: appInstanceId} + const firebaseAppId = '1:1233455666:android:abcdefgh' + const instanceId = {firebase_app_id: firebaseAppId} + const api_secret = '123' + + let errors = formatCheckLib(payload, instanceId, api_secret, true) + + expect(errors[0].description).toEqual( + "Measurement requires an app_instance_id.", + ) + }) }) }) describe("returns invalidEventName errors", () => { test("does not return an error for a valid event name", () => { - const payload = {events: [{name: 'add_payment_info'}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{name: 'add_payment_info'}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -56,7 +139,7 @@ describe("formatCheckLib", () => { }) test("returns an error when event's name is a reserved name", () => { - const payload = {events: [{name: 'ad_click'}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{name: 'ad_click'}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -71,7 +154,7 @@ describe("formatCheckLib", () => { describe("returns invalidUserPropertyName errors", () => { test("does not return an error for a valid user property name", () => { - const payload = {user_properties: {'test': 'test'}} + const payload = {app_instance_id: "12345678901234567890123456789012", user_properties: {'test': 'test'}} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -82,7 +165,7 @@ describe("formatCheckLib", () => { }) test("returns an error when event's name is a reserved name", () => { - const payload = {user_properties: {'first_open_time': 'test'}} + const payload = {app_instance_id: "12345678901234567890123456789012", user_properties: {'first_open_time': 'test'}} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -97,7 +180,7 @@ describe("formatCheckLib", () => { describe("returns invalidCurrencyType errors", () => { test("does not return an error for a valid currency type", () => { - const payload = {events: [{params: {currency: 'USD'}}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{params: {currency: 'USD'}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -108,7 +191,7 @@ describe("formatCheckLib", () => { }) test("returns an error for an invalid currency type", () => { - const payload = {events: [{params: {currency: 'USDD'}}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{params: {currency: 'USDD'}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -123,7 +206,7 @@ describe("formatCheckLib", () => { describe("validates items array", () => { test("does not return an error if items array is valid", () => { - const payload = {events: [{params: {items: [{'item_id': 1234}]}}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{params: {items: [{'item_id': 1234}]}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -134,7 +217,7 @@ describe("formatCheckLib", () => { }) test("returns an error when items does not have either item_id or item_name", () => { - const payload = {events: [{params: {items: [{'item_namee': 'test'}]}}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{params: {items: [{'item_namee': 'test'}]}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -147,7 +230,7 @@ describe("formatCheckLib", () => { }) test("validates empty items array when event requires items", () => { - const payload = {events: [{params: {name: 'purchase', items: []}}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{params: {name: 'purchase', items: []}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -160,7 +243,7 @@ describe("formatCheckLib", () => { }) test("does not validate empty items array when event doesn't require items", () => { - const payload = {events: [{params: {name: 'random', items: []}}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{params: {name: 'random', items: []}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -171,7 +254,7 @@ describe("formatCheckLib", () => { }) test("does not validate empty items array when event does not have a name", () => { - const payload = {events: [{params: {items: []}}]} + const payload = {app_instance_id: "12345678901234567890123456789012", events: [{params: {items: []}}]} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -183,6 +266,7 @@ describe("formatCheckLib", () => { test("returns an error when item_id and item_name keys have empty values", () => { const payload = { + app_instance_id: "12345678901234567890123456789012", events: [ { params: { @@ -211,7 +295,7 @@ describe("formatCheckLib", () => { describe("validates instance_id", () => { describe("when useFirebase is true", () => { test("does not return an error if firebase_app_id is valid", () => { - const payload = {} + const payload = {app_instance_id: "12345678901234567890123456789012"} const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -222,7 +306,7 @@ describe("formatCheckLib", () => { }) test("returns an error when firebase_app_id is invalid", () => { - const payload = {} + const payload = {app_instance_id: "12345678901234567890123456789012"} const firebaseAppId = '1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -235,7 +319,7 @@ describe("formatCheckLib", () => { }) test("does not validate measurement_id", () => { - const payload = {} + const payload = {app_instance_id: "12345678901234567890123456789012"} const firebaseAppId = '1:1233455666:android:abcdefgh' const measurementId = '' const instanceId = {firebase_app_id: firebaseAppId, measurement_id: measurementId} @@ -249,7 +333,7 @@ describe("formatCheckLib", () => { describe("when useFirebase is false", () => { test("does not return an error if measurement_id is not null", () => { - const payload = {} + const payload = {client_id: "12345678901234567890123456789012"} const measurementId = 'test' const instanceId = {measurement_id: measurementId} const api_secret = '123' @@ -260,7 +344,7 @@ describe("formatCheckLib", () => { }) test("returns an error when meausrement_id is null", () => { - const payload = {} + const payload = {client_id: "12345678901234567890123456789012"} const measurementId = '' const instanceId = {measurement_id: measurementId} const api_secret = '123' @@ -273,7 +357,7 @@ describe("formatCheckLib", () => { }) test("does not validate firebase_app_id", () => { - const payload = {} + const payload = {client_id: "12345678901234567890123456789012"} const firebaseAppId = '1233455666:android:abcdefgh' const measurementId = 'test' const instanceId = {firebase_app_id: firebaseAppId, measurement_id: measurementId} @@ -288,7 +372,7 @@ describe("formatCheckLib", () => { describe("validates api_secret", () => { test("does not return an error api_secret is not null", () => { - const payload = {} + const payload = { app_instance_id: "12345678901234567890123456789012" } const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '123' @@ -299,7 +383,7 @@ describe("formatCheckLib", () => { }) test("returns an error when api_secret is null", () => { - const payload = {} + const payload = { app_instance_id: "12345678901234567890123456789012" } const firebaseAppId = '1:1233455666:android:abcdefgh' const instanceId = {firebase_app_id: firebaseAppId} const api_secret = '' diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts index d5c8801d..18232d3e 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts @@ -21,7 +21,7 @@ const RESERVED_USER_PROPERTY_NAMES = [ export const formatCheckLib = (payload, instanceId, api_secret, useFirebase) => { let errors: ValidationMessage[] = [] - const appInstanceIdErrors = isValidAppInstanceId(payload) + const appOrClientErrors = isValidAppOrClientId(payload, useFirebase) const eventNameErrors = isValidEventName(payload) const userPropertyNameErrors = isValidUserPropertyName(payload) const currencyErrors = isValidCurrencyType(payload) @@ -33,7 +33,7 @@ export const formatCheckLib = (payload, instanceId, api_secret, useFirebase) => return [ ...errors, - ...appInstanceIdErrors, + ...appOrClientErrors, ...eventNameErrors, ...userPropertyNameErrors, ...currencyErrors, @@ -45,28 +45,45 @@ export const formatCheckLib = (payload, instanceId, api_secret, useFirebase) => ] } -const isValidAppInstanceId = (payload) => { +const isValidAppOrClientId = (payload, useFirebase) => { let errors: ValidationMessage[] = [] const appInstanceId = payload.app_instance_id + const clientId = payload.client_id - if (appInstanceId) { - if (appInstanceId?.length !== 32) { + if (useFirebase) { + if (appInstanceId) { + if (appInstanceId?.length !== 32) { + errors.push({ + description: `Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${appInstanceId.length}] digits.`, + validationCode: "value_invalid", + fieldPath: "app_instance_id" + }) + } + + if (!appInstanceId.match(/^[A-Fa-f0-9]+$/)) { + let nonChars = appInstanceId.split('').filter((letter: string)=> { + return (!/[0-9A-Fa-f]/.test(letter)) + }) + + errors.push({ + description: `Measurement app_instance_id contains non hexadecimal character [${nonChars[0]}].`, + validationCode: "value_invalid", + fieldPath: "app_instance_id" + }) + } + } else { errors.push({ - description: `Measurement app_instance_id is expected to be a 32 digit hexadecimal number but was [${appInstanceId.length}] digits.`, + description: "Measurement requires an app_instance_id.", validationCode: "value_invalid", fieldPath: "app_instance_id" }) } - - if (!appInstanceId.match(/^[A-Fa-f0-9]+$/)) { - let nonChars = appInstanceId.split('').filter((letter: string)=> { - return (!/[0-9A-Fa-f]/.test(letter)) - }) - + } else { + if (!clientId) { errors.push({ - description: `Measurement app_instance_id contains non hexadecimal character [${nonChars[0]}].`, + description: "Measurement requires a client_id.", validationCode: "value_invalid", - fieldPath: "app_instance_id" + fieldPath: "client_id" }) } } diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts index ee9722ff..4e5d713e 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts @@ -2,10 +2,6 @@ const ALPHA_NUMERIC_NAME = "does not match '^(?!ga_|google_|firebase_)[A-Za-z][A const ALPHA_NUMERIC_OVERRIDE = " may only contain alpha-numeric characters and underscores,start with an alphabetic character, and cannot contain google_, ga_, firebase_" const CUSTOM_PARAMS_NAME = "can have at most [10] custom params." const ITEM_INVALID_KEY_OVERRIDE = "Item array has invalid key" -const ONEOF_SCHEMA_ERROR = "does not match any given oneof schema" -const ONEOF_SCHEMA_ERROR_OVERRIDE_CLIENT_ID = "Measurement requires a client_id." -const ONEOF_SCHEMA_ERROR_OVERRIDE_APP_INSTANCE_ID = "Measurement requires an app_instance_id." - const API_DOC_LIMITATIONS_URL = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?hl=en&client_type=firebase#limitations' const API_DOC_BASE_PAYLOAD_URL = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference#' @@ -13,6 +9,7 @@ const API_DOC_EVENT_URL = 'https://developers.google.com/analytics/devguides/col const API_DOC_USER_PROPERTIES = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?hl=en&client_type=firebase' const API_DOC_SENDING_EVENTS_URL = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?hl=en&client_type=firebase' const API_DOC_JSON_POST_BODY = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=en&client_type=firebase#payload_post_body' +const API_DOC_GTAG = 'https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#required_parameters' const BASE_PAYLOAD_ATTRIBUTES = ['app_instance_id', 'api_secret', 'firebase_app_id', 'user_id', 'timestamp_micros', 'user_properties', 'non_personalized_ads'] @@ -33,18 +30,6 @@ export const formatErrorMessages = (errors, payload, useFirebase) => { return error - } else if (description.endsWith(ONEOF_SCHEMA_ERROR)) { - if (useFirebase) { - error['description'] = ONEOF_SCHEMA_ERROR_OVERRIDE_APP_INSTANCE_ID - error['fieldPath'] = 'app_instance_id' - } else { - error['description'] = ONEOF_SCHEMA_ERROR_OVERRIDE_CLIENT_ID - error['fieldPath'] = 'client_id' - } - - error['validationCode'] = 'VALUE_REQUIRED' - - return error } else if (BASE_PAYLOAD_ATTRIBUTES.includes(fieldPath?.slice(2))) { error['fieldPath'] = fieldPath.slice(2) @@ -56,20 +41,22 @@ export const formatErrorMessages = (errors, payload, useFirebase) => { }) const documentedErrors = formattedErrors.map(error => { - error['documentation'] = addDocumentation(error, payload) + error['documentation'] = addDocumentation(error, payload, useFirebase) return error }) return documentedErrors } -const addDocumentation = (error, payload) => { +const addDocumentation = (error, payload, useFirebase) => { const { fieldPath, validationCode } = error if (validationCode === 'max-length-error' || validationCode === 'max-properties-error' || validationCode === 'max-body-size') { return API_DOC_LIMITATIONS_URL } else if (fieldPath?.startsWith('#/events/')) { return API_DOC_EVENT_URL + payload?.events[0]?.name + } else if (!useFirebase && (fieldPath === 'client_id' || fieldPath === 'measurement_id')) { + return API_DOC_GTAG } else if (BASE_PAYLOAD_ATTRIBUTES.includes(fieldPath)) { return API_DOC_BASE_PAYLOAD_URL + fieldPath } else if (fieldPath === '#/user_properties') { diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts index 69159539..2456ece2 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.spec.ts @@ -1,4 +1,6 @@ import "jest" +import { invalid } from "moment" +import { ValidationStatus } from "../../types" import { Validator } from "../validator" import { baseContentSchema } from "./baseContent" @@ -17,10 +19,14 @@ describe("baseContentSchema", () => { expect(validator.isValid(validInput)).toEqual(true) }) - test("is not valid when an app_instance_id has dashes", () => { + test("is not valid with an additional property", () => { const invalidInput = { - 'app_instance_id': '0239500a-23af-4ab0-a79c-58c4042ea175', - 'events': [] + 'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740', + 'events': [{ + 'name': 'something', + 'params': {} + }], + 'additionalProperty': 123 } let validator = new Validator(baseContentSchema) @@ -28,34 +34,114 @@ describe("baseContentSchema", () => { expect(validator.isValid(invalidInput)).toEqual(false) }) - test("is not valid when an app_instance_id is not 32 chars", () => { - const invalidInput = { - 'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f74', - 'events': [] + test("validates specific event names", () => { + const validInput = { + 'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740', + 'events': [{ + 'name': 'purchase', + 'params': { + 'transaction_id': '894982', + 'value': 89489, + 'currency': 'USD', + 'items': [ + { + 'item_name': 'test' + } + ] + } + }], } let validator = new Validator(baseContentSchema) - expect(validator.isValid(invalidInput)).toEqual(false) + expect(validator.isValid(validInput)).toEqual(true) }) - test("is not valid with an additional property", () => { - const invalidInput = { + test("validates params don't have reserved suffixes", () => { + const validInput = { 'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740', - 'events': [], - 'additionalProperty': 123 + 'events': [{ + 'name': 'purchase', + 'params': { + 'transaction_id': '894982', + 'value': 89489, + 'currency': 'USD', + 'ga_test': '123', + 'items': [ + { + 'item_name': 'test' + } + ] + } + }], } let validator = new Validator(baseContentSchema) - expect(validator.isValid(invalidInput)).toEqual(false) + expect(validator.isValid(validInput)).toEqual(false) + }) + + test("validates required keys are present for certain events", () => { + const validInput = { + 'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740', + 'events': [{ + 'name': 'purchase', + 'params': { + 'transaction_id': '894982', + 'currency': 'USD', + 'items': [ + { + 'item_name': 'test' + } + ] + } + }], + } + + let validator = new Validator(baseContentSchema) + + expect(validator.isValid(validInput)).toEqual(false) }) - test("is not valid without app_instance_id", () => { - const invalidInput = {'events': []} + test("does NOT validate empty item aray for named events, because the error message is too complex and this is validated in formatCheckErrors", () => { + const validInput = { + 'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740', + 'events': [{ + 'name': 'purchase', + 'params': { + 'transaction_id': '894982', + 'value': 89489, + 'currency': 'USD', + 'items': [{}] + } + }], + } let validator = new Validator(baseContentSchema) - expect(validator.isValid(invalidInput)).toEqual(false) + expect(validator.isValid(validInput)).toEqual(true) + }) + + test("validates that items don't have reserved name keys", () => { + const validInput = { + 'app_instance_id': 'bc17b822c8c84a7e84ac7e010cc2f740', + 'events': [{ + 'name': 'purchase', + 'params': { + 'transaction_id': '894982', + 'value': 89489, + 'currency': 'USD', + 'items': [ + { + 'ga_test': 'et' + } + ] + } + }], + } + + let validator = new Validator(baseContentSchema) + + expect(validator.isValid(validInput)).toEqual(false) }) }) \ 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 e4ae2e34..303834fb 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts @@ -3,82 +3,28 @@ import { userPropertiesSchema } from './userProperties' import { eventsSchema } from './events' -// export const baseContentSchema = { -// "type": "object", -// "required": ["app_instance_id", "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, -// } -// } - export const baseContentSchema = { "type": "object", - "oneOf": [ - { - "required": ["app_instance_id", "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, - } + "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" }, - { - "required": ["client_id", "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_properties": userPropertiesSchema, + "non_personalized_ads": { + "type": "boolean" }, - ], + "events": eventsSchema, + } } \ No newline at end of file diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/event.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/event.ts index df35c9e2..db7876ac 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/event.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/event.ts @@ -1,4 +1,5 @@ import { buildEvents } from "./schemaBuilder" +import { itemsSchema } from "./eventTypes/items" export const eventSchema = { "type": "object", @@ -12,7 +13,8 @@ export const eventSchema = { "pattern": "^[A-Za-z][A-Za-z0-9_]*$", "maxLength": 40 }, - "params": {"type": "object"} + "params": {"type": "object"}, + "items": itemsSchema }, "allOf": buildEvents() } diff --git a/src/components/ga4/EventBuilder/ValidateEvent/schemas/eventTypes/item.ts b/src/components/ga4/EventBuilder/ValidateEvent/schemas/eventTypes/item.ts index 1972f3d7..28acca19 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/eventTypes/item.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/eventTypes/item.ts @@ -5,14 +5,15 @@ import { ITEM_FIELDS } from "./fieldDefinitions" export const itemSchema = { "type": "object", - "required": [], "patternProperties": { ".": { "maxLength": 100 } }, "propertyNames": { - "maxLength": 40 + "maxLength": 40, + "pattern": + "^(?!ga_|google_|firebase_)[A-Za-z][A-Za-z0-9_]*$", }, "properties": ITEM_FIELDS, "anyOf": [{