diff --git a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts index 0e298d50..cfc367cf 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.spec.ts @@ -2,61 +2,149 @@ 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 api_secret = '123' - - let errors = formatCheckLib(payload, firebaseAppId, api_secret) - - expect(errors).toEqual([]) + 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) + + 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' + + 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.` + ) + }) + + 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 is not 32 chars", () => { - const appInstanceId = "123456789012345678901234567890123" - const payload = {app_instance_id: appInstanceId} - const firebaseAppId = '1:1233455666:android:abcdefgh' - const api_secret = '123' - - let errors = formatCheckLib(payload, firebaseAppId, api_secret) - - 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 api_secret = '123' - - let errors = formatCheckLib(payload, firebaseAppId, api_secret) - - expect(errors[0].description).toEqual( - `Measurement app_instance_id contains non hexadecimal character [g].`, - ) + 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, false) + + 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' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) 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' - 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" @@ -66,21 +154,23 @@ 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' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) 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' - 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" @@ -90,21 +180,23 @@ 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' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) 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' - 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" @@ -114,21 +206,23 @@ 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' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) 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 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' - 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'" @@ -136,11 +230,12 @@ 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' - 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" @@ -148,27 +243,30 @@ 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' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) 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' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) test("returns an error when item_id and item_name keys have empty values", () => { const payload = { + app_instance_id: "12345678901234567890123456789012", events: [ { params: { @@ -183,9 +281,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,47 +292,103 @@ 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 = {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([]) + expect(errors).toEqual([]) + }) + + test("returns an error when firebase_app_id is invalid", () => { + const payload = {app_instance_id: "12345678901234567890123456789012"} + 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 = {app_instance_id: "12345678901234567890123456789012"} + 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 = {client_id: "12345678901234567890123456789012"} + 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 = {client_id: "12345678901234567890123456789012"} + 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 = {client_id: "12345678901234567890123456789012"} + 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([]) + }) }) }) 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' - let errors = formatCheckLib(payload, firebaseAppId, api_secret) + let errors = formatCheckLib(payload, instanceId, api_secret, true) expect(errors).toEqual([]) }) 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 = '' - 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..18232d3e 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/formatCheckLib.ts @@ -18,55 +18,72 @@ 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) + const appOrClientErrors = isValidAppOrClientId(payload, useFirebase) const eventNameErrors = isValidEventName(payload) const userPropertyNameErrors = isValidUserPropertyName(payload) 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) return [ ...errors, - ...appInstanceIdErrors, + ...appOrClientErrors, ...eventNameErrors, ...userPropertyNameErrors, ...currencyErrors, ...emptyItemsErrors, ...itemsRequiredKeyErrors, - ...firebaseAppIdErrors, + ...instanceIdErrors, ...apiSecretErrors, ...sizeErrors, ] } -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" }) } } @@ -182,15 +199,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/handlers/responseUtil.ts b/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts index e233d339..4e5d713e 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/handlers/responseUtil.ts @@ -9,11 +9,12 @@ 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'] // 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 @@ -28,6 +29,7 @@ export const formatErrorMessages = (errors, payload) => { error['description'] = description.slice(0, end_index) + ALPHA_NUMERIC_OVERRIDE return error + } else if (BASE_PAYLOAD_ATTRIBUTES.includes(fieldPath?.slice(2))) { error['fieldPath'] = fieldPath.slice(2) @@ -39,20 +41,22 @@ export const formatErrorMessages = (errors, payload) => { }) 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 6a98b5f2..303834fb 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/schemas/baseContent.ts @@ -1,18 +1,20 @@ // 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' export const baseContentSchema = { "type": "object", - "required": ["app_instance_id", "events"], + "required": ["events"], "additionalProperties": false, "properties": { "app_instance_id": { "type": "string", "format": "app_instance_id" }, + "client_id": { + "type": "string", + }, "user_id": { "type": "string" }, @@ -25,4 +27,4 @@ export const baseContentSchema = { }, "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": [{ diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts b/src/components/ga4/EventBuilder/ValidateEvent/useValidateEvent.ts index 48ae3f92..b33d785c 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 => { @@ -171,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) 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"