From b2c62e7d801c3edfc6deeaa31e64114e7dfda751 Mon Sep 17 00:00:00 2001 From: hardikmashru Date: Wed, 19 Jun 2024 17:58:41 +0530 Subject: [PATCH 1/8] Fixed single item matches code --- react-example/src/views/AUTTesting.tsx | 7 +- .../criteriaCompletionChecker.ts | 132 ++++++++++-------- 2 files changed, 80 insertions(+), 59 deletions(-) diff --git a/react-example/src/views/AUTTesting.tsx b/react-example/src/views/AUTTesting.tsx index 8135b584..02151417 100644 --- a/react-example/src/views/AUTTesting.tsx +++ b/react-example/src/views/AUTTesting.tsx @@ -30,7 +30,7 @@ export const AUTTesting: FC = () => { ); const [cartItem, setCartItem] = useState( - '{"items":[{"name":"piano","id":"fdsafds","price":100,"quantity":2}]}' + '{"items":[{"name":"piano","id":"fdsafds","price":100,"quantity":2}, {"name":"piano2","id":"fdsafds2","price":100,"quantity":5}]}' ); const [purchaseItem, setPurchaseItem] = useState( @@ -59,7 +59,6 @@ export const AUTTesting: FC = () => { const handleParseJson = (isUpdateCartCalled: boolean) => { try { // Parse JSON and assert its type - // {"items":[{"name":"piano","id":"fdsafds","price":100,"quantity":2}]} if (isUpdateCartCalled) { const parsedObject = JSON.parse(cartItem) as UpdateCartRequestParams; return parsedObject; @@ -109,11 +108,11 @@ export const AUTTesting: FC = () => { const handleTrackPurchase = (e: FormEvent) => { e.preventDefault(); - const jsonObj = handleParseJson(false); + const jsonObj: TrackPurchaseRequestParams = handleParseJson(false); if (jsonObj) { setTrackingPurchase(true); try { - trackPurchase({ ...jsonObj, total: 20 }) + trackPurchase(jsonObj) .then((response: any) => { setTrackingPurchase(false); setTrackPurchaseResponse(JSON.stringify(response.data)); diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index 3e2a6aaa..6875f8cb 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -234,8 +234,12 @@ class CriteriaCompletionChecker { return false; } } else if (node.searchCombo) { - const vv = this.evaluateSearchQueries(node, localEventData, criteriaId); - return vv; + const result = this.evaluateSearchQueries( + node, + localEventData, + criteriaId + ); + return result; } } catch (e) { this.handleException(e); @@ -290,8 +294,12 @@ class CriteriaCompletionChecker { nodeCombo: { searchCombo: object; count: number }[]; }) => item.criteriaId === criteriaId ); - - if (this.evaluateEvent(eventData, searchQueries, combinator)) { + const result = this.evaluateEvent( + eventData, + searchQueries, + combinator + ); + if (result) { if (Object.prototype.hasOwnProperty.call(node, 'minMatch')) { const matchedNode = matchedCriteria && @@ -353,7 +361,11 @@ class CriteriaCompletionChecker { SHARED_PREF_MATCHED_CRITERIAS, JSON.stringify(tempMatchedCriterias) ); - return false; + if (node.minMatch === 1) { + return true; + } else { + return false; + } } } else { return true; @@ -371,73 +383,83 @@ class CriteriaCompletionChecker { combinator: string ): boolean { if (combinator === 'And') { - for (let i = 0; i < searchQueries.length; i++) { - if (!this.evaluateFieldLogic(searchQueries[i], localEvent)) { - return false; - } + if (!this.evaluateFieldLogic(searchQueries, localEvent)) { + return false; } return true; } else if (combinator === 'Or') { - for (let i = 0; i < searchQueries.length; i++) { - if (this.evaluateFieldLogic(searchQueries[i], localEvent)) { - return true; - } + if (this.evaluateFieldLogic(searchQueries, localEvent)) { + return true; } return false; } return false; } - private evaluateFieldLogic(node: any, eventData: any): boolean { - const field = node.field; - const comparatorType = node.comparatorType ? node.comparatorType : ''; + private evaluateFieldLogic(searchQueries: any[], eventData: any): boolean { const localDataKeys = Object.keys(eventData); - let shouldReturn = false; - for (let j = 0; j < localDataKeys.length; j++) { - const key = localDataKeys[j]; - if (key === KEY_ITEMS) { - // scenario of items inside purchase and updateCart Events - const items = eventData[key]; - items.forEach((item: any) => { - const keys = Object.keys(item); - keys.forEach((keyItem: any) => { - if (field === keyItem) { - const matchedCountObj = item[keyItem]; - if ( - this.evaluateComparison( - comparatorType, - matchedCountObj, - node.value ? node.value : '' - ) - ) { - shouldReturn = true; - return; - } - } - }); - if (shouldReturn) return; // Exit outer forEach loop - }); - if (shouldReturn) break; // Exit main for loop - } else { + let combinedResult = false; + if (localDataKeys.includes(KEY_ITEMS)) { + // scenario of items inside purchase and updateCart Events + const items = eventData[KEY_ITEMS]; + const result = items.some((item: any) => { + return this.doesItemMatchQueries(item, searchQueries); + }); + if (!result) { + return result; + } + combinedResult = result; + } + const filteredLocalDataKeys = localDataKeys.filter( + (item: any) => item !== KEY_ITEMS + ); + const filteredSearchQueries = searchQueries.filter((searchQuery) => + filteredLocalDataKeys.includes(searchQuery.field) + ); + if (filteredSearchQueries.length === 0) { + return combinedResult; + } + for (let index = 0; index < filteredLocalDataKeys.length; index++) { + const key = filteredLocalDataKeys[index]; + const result = filteredSearchQueries.some((query: any) => { + const field = query.field; + if (field === key) { - const matchedCountObj = eventData[key]; - if ( - this.evaluateComparison( - comparatorType, - matchedCountObj, - node.value ? node.value : '' - ) - ) { - return true; + if (Object.prototype.hasOwnProperty.call(eventData, field)) { + const result = this.evaluateComparison( + query.comparatorType, + eventData[field], + query.value ? query.value : '' + ); + return result; } } + }); + if (result) { + return result; } } + return false; + } - if (shouldReturn) { - return true; + private doesItemMatchQueries(item: any, searchQueries: any[]): boolean { + const filteredSearchQueries = searchQueries.filter((searchQuery) => + Object.keys(item).includes(searchQuery.field) + ); + if (filteredSearchQueries.length === 0) { + return false; } - return false; + return filteredSearchQueries.every((query: any) => { + const field = query.field; + if (Object.prototype.hasOwnProperty.call(item, field)) { + return this.evaluateComparison( + query.comparatorType, + item[field], + query.value ? query.value : '' + ); + } + return false; + }); } private evaluateComparison( From 2effb954bedae05b3f173f394dcbc88ccd6c1078 Mon Sep 17 00:00:00 2001 From: hardikmashru Date: Wed, 19 Jun 2024 18:36:28 +0530 Subject: [PATCH 2/8] Fixed comments --- .../criteriaCompletionChecker.ts | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index 6875f8cb..bf588f48 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -234,12 +234,7 @@ class CriteriaCompletionChecker { return false; } } else if (node.searchCombo) { - const result = this.evaluateSearchQueries( - node, - localEventData, - criteriaId - ); - return result; + return this.evaluateSearchQueries(node, localEventData, criteriaId); } } catch (e) { this.handleException(e); @@ -294,12 +289,7 @@ class CriteriaCompletionChecker { nodeCombo: { searchCombo: object; count: number }[]; }) => item.criteriaId === criteriaId ); - const result = this.evaluateEvent( - eventData, - searchQueries, - combinator - ); - if (result) { + if (this.evaluateEvent(eventData, searchQueries, combinator)) { if (Object.prototype.hasOwnProperty.call(node, 'minMatch')) { const matchedNode = matchedCriteria && @@ -338,11 +328,7 @@ class CriteriaCompletionChecker { JSON.stringify(this.localStoredEventList) ); - if (matchedNode[0].count === node.minMatch) { - return true; - } else { - return false; - } + return matchedNode[0].count === node.minMatch; } else { const tempMatchedCriterias = matchedCriterias || []; tempMatchedCriterias.push({ @@ -361,11 +347,7 @@ class CriteriaCompletionChecker { SHARED_PREF_MATCHED_CRITERIAS, JSON.stringify(tempMatchedCriterias) ); - if (node.minMatch === 1) { - return true; - } else { - return false; - } + return node.minMatch === 1; } } else { return true; @@ -398,7 +380,7 @@ class CriteriaCompletionChecker { private evaluateFieldLogic(searchQueries: any[], eventData: any): boolean { const localDataKeys = Object.keys(eventData); - let combinedResult = false; + let itemMatchedResult = false; if (localDataKeys.includes(KEY_ITEMS)) { // scenario of items inside purchase and updateCart Events const items = eventData[KEY_ITEMS]; @@ -408,7 +390,7 @@ class CriteriaCompletionChecker { if (!result) { return result; } - combinedResult = result; + itemMatchedResult = result; } const filteredLocalDataKeys = localDataKeys.filter( (item: any) => item !== KEY_ITEMS @@ -417,26 +399,25 @@ class CriteriaCompletionChecker { filteredLocalDataKeys.includes(searchQuery.field) ); if (filteredSearchQueries.length === 0) { - return combinedResult; + return itemMatchedResult; } for (let index = 0; index < filteredLocalDataKeys.length; index++) { const key = filteredLocalDataKeys[index]; - const result = filteredSearchQueries.some((query: any) => { + const filteredResult = filteredSearchQueries.some((query: any) => { const field = query.field; if (field === key) { if (Object.prototype.hasOwnProperty.call(eventData, field)) { - const result = this.evaluateComparison( + return this.evaluateComparison( query.comparatorType, eventData[field], query.value ? query.value : '' ); - return result; } } }); - if (result) { - return result; + if (filteredResult) { + return filteredResult; } } return false; From 3bcb00391e5219de3a6c11eb073674532034aa3e Mon Sep 17 00:00:00 2001 From: Hardik Mashru Date: Mon, 24 Jun 2024 17:19:44 +0530 Subject: [PATCH 3/8] added more unit tests, bug fixes --- .../criteriaCompletionChecker.test.ts | 329 +++++++++++++++++- .../criteriaCompletionChecker.ts | 73 ++-- src/constants.ts | 3 + 3 files changed, 365 insertions(+), 40 deletions(-) diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.test.ts b/src/anonymousUserTracking/criteriaCompletionChecker.test.ts index ff18bb84..70b5d460 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.test.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.test.ts @@ -31,14 +31,14 @@ describe('CriteriaCompletionChecker', () => { expect(result).toBeNull(); }); - it('should return criteriaId if criteriaData condition is matched', () => { + it('should return criteriaId if customEvent is matched', () => { (localStorage.getItem as jest.Mock).mockImplementation((key) => { if (key === SHARED_PREFS_EVENT_LIST_KEY) { return JSON.stringify([ { eventName: 'testEvent', createdAt: 1708494757530, - dataFields: undefined, + dataFields: { 'browserVisit.website.domain': 'google.com' }, createNewFields: true, eventType: 'customEvent' } @@ -80,6 +80,13 @@ describe('CriteriaCompletionChecker', () => { comparatorType: 'Equals', value: 'testEvent', fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'browserVisit.website.domain', + comparatorType: 'Equals', + value: 'google.com', + fieldType: 'string' } ] } @@ -95,14 +102,14 @@ describe('CriteriaCompletionChecker', () => { expect(result).toEqual('6'); }); - it('should return null if criteriaData condition is matched', () => { + it('should return criteriaId if customEvent is matched when minMatch present', () => { (localStorage.getItem as jest.Mock).mockImplementation((key) => { if (key === SHARED_PREFS_EVENT_LIST_KEY) { return JSON.stringify([ { - eventName: 'Event', + eventName: 'testEvent', createdAt: 1708494757530, - dataFields: undefined, + dataFields: { 'browserVisit.website.domain': 'google.com' }, createNewFields: true, eventType: 'customEvent' } @@ -134,7 +141,9 @@ describe('CriteriaCompletionChecker', () => { combinator: 'Or', searchQueries: [ { - dataType: 'purchase', + dataType: 'customEvent', + minMatch: 1, + maxMatch: 2, searchCombo: { combinator: 'And', searchQueries: [ @@ -144,6 +153,243 @@ describe('CriteriaCompletionChecker', () => { comparatorType: 'Equals', value: 'testEvent', fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'browserVisit.website.domain', + comparatorType: 'Equals', + value: 'google.com', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + }); + + it('should return criteriaId if purchase event is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + createdAt: 1708494757530, + items: [ + { name: 'keyboard', id: 'fdsafds', price: 10, quantity: 2 } + ], + total: 10, + eventType: 'purchase' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'keyboard', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '10', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + }); + + it('should return null if updateCart event with all props in item is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + createdAt: 1708494757530, + items: [ + { name: 'keyboard', id: 'fdsafds', price: 10, quantity: 2 }, + { name: 'Cofee', id: 'fdsafds', price: 10, quantity: 2 } + ], + eventType: 'cartUpdate' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'updateCart', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'keyboard', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '10', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + }); + + it('should return null if updateCart event with items is NOT matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + createdAt: 1708494757530, + items: [ + { name: 'keyboard', id: 'fdsafds', price: 9, quantity: 2 }, + { name: 'Cofee', id: 'fdsafds', price: 10, quantity: 2 } + ], + eventType: 'cartUpdate' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'updateCart', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'keyboard', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '10', + fieldType: 'double' } ] } @@ -159,6 +405,77 @@ describe('CriteriaCompletionChecker', () => { expect(result).toBeNull(); }); + it('should return criteriaId if updateCart event is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + createdAt: 1708494757530, + items: [ + { name: 'keyboard', id: 'fdsafds', price: 10, quantity: 2 } + ], + eventType: 'cartUpdate' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'updateCart', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '10', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + }); + it('should return criteriaId if criteriaData condition with numeric is matched', () => { (localStorage.getItem as jest.Mock).mockImplementation((key) => { if (key === SHARED_PREFS_EVENT_LIST_KEY) { diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index bf588f48..5eed0f4c 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -7,6 +7,8 @@ import { UPDATE_CART, UPDATE_USER, KEY_EVENT_NAME, + UPDATECART_ITEM_PREFIX, + PURCHASE_ITEM_PREFIX, SHARED_PREFS_EVENT_LIST_KEY, SHARED_PREF_MATCHED_CRITERIAS } from '../constants'; @@ -110,7 +112,7 @@ class CriteriaCompletionChecker { items = items.map((item: any) => { const updatItem: any = {}; Object.keys(item).forEach((key) => { - updatItem[`shoppingCartItems.${key}`] = item[key]; + updatItem[`${PURCHASE_ITEM_PREFIX}${key}`] = item[key]; }); return updatItem; }); @@ -143,8 +145,7 @@ class CriteriaCompletionChecker { items = items.map((item: any) => { const updatItem: any = {}; Object.keys(item).forEach((key) => { - updatItem[`updateCart.updatedShoppingCartItems.${key}`] = - item[key]; + updatItem[`${UPDATECART_ITEM_PREFIX}${key}`] = item[key]; }); return updatItem; }); @@ -155,6 +156,7 @@ class CriteriaCompletionChecker { Object.keys(localEventData.dataFields).forEach((key) => { updatedItem[key] = localEventData.dataFields[key]; }); + delete localEventData.dataFields; } Object.keys(localEventData).forEach((key) => { if (key !== KEY_ITEMS && key !== 'dataFields') { @@ -198,6 +200,7 @@ class CriteriaCompletionChecker { Object.keys(localEventData.dataFields).forEach((key) => { updatedItem[key] = localEventData.dataFields[key]; }); + delete localEventData.dataFields; } nonPurchaseEvents.push(updatedItem); } @@ -257,13 +260,6 @@ class CriteriaCompletionChecker { const searchCombo = node.searchCombo; const searchQueries = searchCombo?.searchQueries || []; const combinator = searchCombo?.combinator || ''; - // matchedCriterias format - // [ - // { - // criteriaId: '6', - // nodeCombo: [{searchCombo: {}, count: 1}], - // }, - // ]; const matchedCriteriasFromLocalStorage = localStorage.getItem( SHARED_PREF_MATCHED_CRITERIAS ); @@ -378,6 +374,15 @@ class CriteriaCompletionChecker { return false; } + private doesItemCriteriaExists(searchQueries: any[]): boolean { + const foundIndex = searchQueries.findIndex( + (item) => + item.field.startsWith(UPDATECART_ITEM_PREFIX) || + item.field.startsWith(PURCHASE_ITEM_PREFIX) + ); + return foundIndex !== -1; + } + private evaluateFieldLogic(searchQueries: any[], eventData: any): boolean { const localDataKeys = Object.keys(eventData); let itemMatchedResult = false; @@ -387,7 +392,8 @@ class CriteriaCompletionChecker { const result = items.some((item: any) => { return this.doesItemMatchQueries(item, searchQueries); }); - if (!result) { + if (!result && this.doesItemCriteriaExists(searchQueries)) { + // items criteria existed and it did not match return result; } itemMatchedResult = result; @@ -395,32 +401,31 @@ class CriteriaCompletionChecker { const filteredLocalDataKeys = localDataKeys.filter( (item: any) => item !== KEY_ITEMS ); - const filteredSearchQueries = searchQueries.filter((searchQuery) => - filteredLocalDataKeys.includes(searchQuery.field) - ); - if (filteredSearchQueries.length === 0) { + + if (filteredLocalDataKeys.length === 0) { return itemMatchedResult; } - for (let index = 0; index < filteredLocalDataKeys.length; index++) { - const key = filteredLocalDataKeys[index]; - const filteredResult = filteredSearchQueries.some((query: any) => { - const field = query.field; - - if (field === key) { - if (Object.prototype.hasOwnProperty.call(eventData, field)) { - return this.evaluateComparison( - query.comparatorType, - eventData[field], - query.value ? query.value : '' - ); - } - } - }); - if (filteredResult) { - return filteredResult; + + const filteredSearchQueries = searchQueries.filter( + (searchQuery) => + !searchQuery.field.startsWith(UPDATECART_ITEM_PREFIX) && + !searchQuery.field.startsWith(PURCHASE_ITEM_PREFIX) + ); + const matchResult = filteredSearchQueries.every((query: any) => { + const field = query.field; + const eventKeyItems = filteredLocalDataKeys.filter( + (keyItem) => keyItem === field + ); + if (eventKeyItems.length) { + return this.evaluateComparison( + query.comparatorType, + eventData[field], + query.value ? query.value : '' + ); } - } - return false; + return false; + }); + return matchResult; } private doesItemMatchQueries(item: any, searchQueries: any[]): boolean { diff --git a/src/constants.ts b/src/constants.ts index 8c695ed9..07d2c234 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -302,6 +302,9 @@ export const UPDATE_USER = 'user'; export const TRACK_UPDATE_CART = 'cartUpdate'; export const UPDATE_CART = 'updateCart'; +export const UPDATECART_ITEM_PREFIX = 'updateCart.updatedShoppingCartItems.'; +export const PURCHASE_ITEM_PREFIX = 'shoppingCartItems.'; + export const MERGE_SUCCESSFULL = 'MERGE_SUCCESSFULL'; export const INITIALIZE_ERROR = 'Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods'; From 808a03ead6b40d4fb2a959d5e9f6cbd3564bae3d Mon Sep 17 00:00:00 2001 From: hardikmashru Date: Mon, 1 Jul 2024 16:44:58 +0530 Subject: [PATCH 4/8] Not combinator implemented --- .../criteriaCompletionChecker.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index 5eed0f4c..17852922 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -235,6 +235,16 @@ class CriteriaCompletionChecker { } } return false; + } else if (combinator === 'Not') { + for (let i = 0; i < searchQueries.length; i++) { + searchQueries[i]['isNot'] = true; + if ( + this.evaluateTree(searchQueries[i], localEventData, criteriaId) + ) { + return false; + } + } + return true; } } else if (node.searchCombo) { return this.evaluateSearchQueries(node, localEventData, criteriaId); @@ -255,6 +265,7 @@ class CriteriaCompletionChecker { const eventData = localEventData[i]; const trackingType = eventData[SHARED_PREFS_EVENT_TYPE]; const dataType = node.dataType; + const isNot = Object.prototype.hasOwnProperty.call(node, 'isNot'); if (!Object.prototype.hasOwnProperty.call(eventData, 'criteriaId')) { if (dataType === trackingType) { const searchCombo = node.searchCombo; @@ -346,8 +357,13 @@ class CriteriaCompletionChecker { return node.minMatch === 1; } } else { + if (isNot && !(i + 1 === localEventData.length)) { + continue; + } return true; } + } else if (isNot) { + return false; } } } From dce3eb46e1148272856354c2ecc7614b5d43428e Mon Sep 17 00:00:00 2001 From: hardikmashru Date: Mon, 1 Jul 2024 18:35:50 +0530 Subject: [PATCH 5/8] evaluateEvent added not combinator --- src/anonymousUserTracking/criteriaCompletionChecker.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index 17852922..8d59ae17 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -386,6 +386,10 @@ class CriteriaCompletionChecker { return true; } return false; + } else if (combinator === 'Not') { + if (!this.evaluateFieldLogic(searchQueries, localEvent)) { + return true; + } } return false; } From b17ef7a88c4eed6678f26e4c0597811d8170e67b Mon Sep 17 00:00:00 2001 From: hardikmashru Date: Tue, 2 Jul 2024 15:42:12 +0530 Subject: [PATCH 6/8] minMatch changes revert --- .../anonymousUserEventManager.ts | 3 - .../criteriaCompletionChecker.ts | 166 +++--------------- src/constants.ts | 1 - 3 files changed, 21 insertions(+), 149 deletions(-) diff --git a/src/anonymousUserTracking/anonymousUserEventManager.ts b/src/anonymousUserTracking/anonymousUserEventManager.ts index 4d6f8324..1fa7a844 100644 --- a/src/anonymousUserTracking/anonymousUserEventManager.ts +++ b/src/anonymousUserTracking/anonymousUserEventManager.ts @@ -22,7 +22,6 @@ import { ENDPOINT_TRACK_ANON_SESSION, WEB_PLATFORM, KEY_PREFER_USERID, - SHARED_PREF_MATCHED_CRITERIAS, ENDPOINTS } from 'src/constants'; import { baseIterableRequest } from 'src/request'; @@ -221,7 +220,6 @@ export class AnonymousUserEventManager { if (trackEventList.length) { trackEventList.forEach((event: any) => { const eventType = event[SHARED_PREFS_EVENT_TYPE]; - delete event.criteriaId; delete event.eventType; switch (eventType) { case TRACK_EVENT: { @@ -246,7 +244,6 @@ export class AnonymousUserEventManager { localStorage.removeItem(SHARED_PREFS_ANON_SESSIONS); localStorage.removeItem(SHARED_PREFS_EVENT_LIST_KEY); - localStorage.removeItem(SHARED_PREF_MATCHED_CRITERIAS); }); } } diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index 5eed0f4c..61432965 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -8,9 +8,7 @@ import { UPDATE_USER, KEY_EVENT_NAME, UPDATECART_ITEM_PREFIX, - PURCHASE_ITEM_PREFIX, - SHARED_PREFS_EVENT_LIST_KEY, - SHARED_PREF_MATCHED_CRITERIAS + PURCHASE_ITEM_PREFIX } from '../constants'; interface SearchQuery { @@ -57,17 +55,12 @@ class CriteriaCompletionChecker { } private findMatchedCriteria(criteriaList: Criteria[]): string | null { - const criteriaIdList = criteriaList.map((criteria) => criteria.criteriaId); - const eventsToProcess = this.prepareEventsToProcess(criteriaIdList); + const eventsToProcess = this.prepareEventsToProcess(); // Use find to get the first matching criteria const matchingCriteria = criteriaList.find((criteria) => { if (criteria.searchQuery && criteria.criteriaId) { - return this.evaluateTree( - criteria.searchQuery, - eventsToProcess, - criteria.criteriaId - ); + return this.evaluateTree(criteria.searchQuery, eventsToProcess); } return false; }); @@ -76,9 +69,9 @@ class CriteriaCompletionChecker { return matchingCriteria ? matchingCriteria.criteriaId : null; } - private prepareEventsToProcess(criteriaIdList: string[]): any[] { - const eventsToProcess: any[] = this.getEventsWithCartItems(criteriaIdList); - const nonPurchaseEvents: any[] = this.getNonCartEvents(criteriaIdList); + private prepareEventsToProcess(): any[] { + const eventsToProcess: any[] = this.getEventsWithCartItems(); + const nonPurchaseEvents: any[] = this.getNonCartEvents(); nonPurchaseEvents.forEach((event) => { eventsToProcess.push(event); @@ -87,20 +80,10 @@ class CriteriaCompletionChecker { return eventsToProcess; } - private getEventsWithCartItems(criteriaIdList: string[]): any[] { + private getEventsWithCartItems(): any[] { const processedEvents: any[] = []; - this.localStoredEventList.forEach((localEventData, index) => { - if (Object.prototype.hasOwnProperty.call(localEventData, 'criteriaId')) { - if (!criteriaIdList.includes(localEventData.criteriaId)) { - delete localEventData.criteriaId; - this.localStoredEventList[index] = localEventData; - localStorage.setItem( - SHARED_PREFS_EVENT_LIST_KEY, - JSON.stringify(this.localStoredEventList) - ); - } - } + this.localStoredEventList.forEach((localEventData) => { if ( localEventData[SHARED_PREFS_EVENT_TYPE] && localEventData[SHARED_PREFS_EVENT_TYPE] === TRACK_PURCHASE @@ -177,19 +160,9 @@ class CriteriaCompletionChecker { return processedEvents; } - private getNonCartEvents(criteriaIdList: string[]): any[] { + private getNonCartEvents(): any[] { const nonPurchaseEvents: any[] = []; - this.localStoredEventList.forEach((localEventData, index) => { - if (Object.prototype.hasOwnProperty.call(localEventData, 'criteriaId')) { - if (!criteriaIdList.includes(localEventData.criteriaId)) { - delete localEventData.criteriaId; - this.localStoredEventList[index] = localEventData; - localStorage.setItem( - SHARED_PREFS_EVENT_LIST_KEY, - JSON.stringify(this.localStoredEventList) - ); - } - } + this.localStoredEventList.forEach((localEventData) => { if ( localEventData[SHARED_PREFS_EVENT_TYPE] && (localEventData[SHARED_PREFS_EVENT_TYPE] === UPDATE_USER || @@ -208,36 +181,28 @@ class CriteriaCompletionChecker { return nonPurchaseEvents; } - private evaluateTree( - node: SearchQuery, - localEventData: any[], - criteriaId: string - ): boolean { + private evaluateTree(node: SearchQuery, localEventData: any[]): boolean { try { if (node.searchQueries) { const combinator = node.combinator; const searchQueries: any = node.searchQueries; if (combinator === 'And') { for (let i = 0; i < searchQueries.length; i++) { - if ( - !this.evaluateTree(searchQueries[i], localEventData, criteriaId) - ) { + if (!this.evaluateTree(searchQueries[i], localEventData)) { return false; } } return true; } else if (combinator === 'Or') { for (let i = 0; i < searchQueries.length; i++) { - if ( - this.evaluateTree(searchQueries[i], localEventData, criteriaId) - ) { + if (this.evaluateTree(searchQueries[i], localEventData)) { return true; } } return false; } } else if (node.searchCombo) { - return this.evaluateSearchQueries(node, localEventData, criteriaId); + return this.evaluateSearchQueries(node, localEventData); } } catch (e) { this.handleException(e); @@ -247,108 +212,19 @@ class CriteriaCompletionChecker { private evaluateSearchQueries( node: SearchQuery, - localEventData: any[], - criteriaId: string + localEventData: any[] ): boolean { // this function will compare the actualy searhqueues under search combo for (let i = 0; i < localEventData.length; i++) { const eventData = localEventData[i]; const trackingType = eventData[SHARED_PREFS_EVENT_TYPE]; const dataType = node.dataType; - if (!Object.prototype.hasOwnProperty.call(eventData, 'criteriaId')) { - if (dataType === trackingType) { - const searchCombo = node.searchCombo; - const searchQueries = searchCombo?.searchQueries || []; - const combinator = searchCombo?.combinator || ''; - const matchedCriteriasFromLocalStorage = localStorage.getItem( - SHARED_PREF_MATCHED_CRITERIAS - ); - - const matchedCriterias = - matchedCriteriasFromLocalStorage && - JSON.parse(matchedCriteriasFromLocalStorage); - - const matchedCriteria = - matchedCriterias && - matchedCriterias.find( - (item: { - criteriaId: string; - nodeCombo: { searchCombo: object; count: number }[]; - }) => item.criteriaId === criteriaId - ); - - const matchedCriteriaIndex = - matchedCriterias && - matchedCriterias.findIndex( - (item: { - criteriaId: string; - nodeCombo: { searchCombo: object; count: number }[]; - }) => item.criteriaId === criteriaId - ); - if (this.evaluateEvent(eventData, searchQueries, combinator)) { - if (Object.prototype.hasOwnProperty.call(node, 'minMatch')) { - const matchedNode = - matchedCriteria && - matchedCriteria.nodeCombo.filter( - (n: { searchCombo: object; count: number }) => - JSON.stringify(n.searchCombo) === - JSON.stringify(node.searchCombo) - ); - if (matchedNode && matchedNode.length > 0) { - // Update the count of the first node found - matchedNode[0].count = (matchedNode[0].count || 0) + 1; - // Find the index of the node in matchedCriteria.nodeCombo - const nodeIndex = matchedCriteria.nodeCombo.findIndex( - (n: { searchCombo: object; count: number }) => - JSON.stringify(n.searchCombo) === - JSON.stringify(matchedNode[0].searchCombo) - ); - - if (nodeIndex !== -1) { - // Update the node in the matchedCriteria.nodeCombo array - matchedCriteria.nodeCombo[nodeIndex] = matchedNode[0]; - matchedCriterias[matchedCriteriaIndex] = matchedCriteria; - } - // Update local storage with the new matchedCriteria - localStorage.setItem( - SHARED_PREF_MATCHED_CRITERIAS, - JSON.stringify(matchedCriterias) - ); - - const eventFromLocal = this.localStoredEventList[i]; - eventFromLocal.criteriaId = criteriaId; - this.localStoredEventList[i] = eventFromLocal; - - localStorage.setItem( - SHARED_PREFS_EVENT_LIST_KEY, - JSON.stringify(this.localStoredEventList) - ); - - return matchedNode[0].count === node.minMatch; - } else { - const tempMatchedCriterias = matchedCriterias || []; - tempMatchedCriterias.push({ - criteriaId: criteriaId, - nodeCombo: [{ searchCombo: node.searchCombo, count: 1 }] - }); - const eventFromLocal = this.localStoredEventList[i]; - eventFromLocal.criteriaId = criteriaId; - this.localStoredEventList[i] = eventFromLocal; - - localStorage.setItem( - SHARED_PREFS_EVENT_LIST_KEY, - JSON.stringify(this.localStoredEventList) - ); - localStorage.setItem( - SHARED_PREF_MATCHED_CRITERIAS, - JSON.stringify(tempMatchedCriterias) - ); - return node.minMatch === 1; - } - } else { - return true; - } - } + if (dataType === trackingType) { + const searchCombo = node.searchCombo; + const searchQueries = searchCombo?.searchQueries || []; + const combinator = searchCombo?.combinator || ''; + if (this.evaluateEvent(eventData, searchQueries, combinator)) { + return true; } } } diff --git a/src/constants.ts b/src/constants.ts index 07d2c234..3685b112 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -285,7 +285,6 @@ export const SHARED_PREFS_EVENT_LIST_KEY = 'itbl_event_list'; export const SHARED_PREFS_CRITERIA = 'criteria'; export const SHARED_PREFS_ANON_SESSIONS = 'itbl_anon_sessions'; export const SHARED_PREF_ANON_USER_ID = 'anon_userId'; -export const SHARED_PREF_MATCHED_CRITERIAS = 'matchedCriterias'; export const KEY_EVENT_NAME = 'eventName'; export const KEY_CREATED_AT = 'createdAt'; From c3ec160060310441bc30b1d0d528d4268e37fe21 Mon Sep 17 00:00:00 2001 From: hardikmashru Date: Tue, 2 Jul 2024 16:24:39 +0530 Subject: [PATCH 7/8] minMatch implemented --- src/anonymousUserTracking/criteriaCompletionChecker.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index 61432965..98b5aa1c 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -224,6 +224,13 @@ class CriteriaCompletionChecker { const searchQueries = searchCombo?.searchQueries || []; const combinator = searchCombo?.combinator || ''; if (this.evaluateEvent(eventData, searchQueries, combinator)) { + if (node.minMatch) { + const minMatch = node.minMatch - 1; + node.minMatch = minMatch; + if (minMatch > 0) { + continue; + } + } return true; } } From 0f446480548593cc848717144583b19015231b6a Mon Sep 17 00:00:00 2001 From: hardikmashru Date: Tue, 2 Jul 2024 20:15:06 +0530 Subject: [PATCH 8/8] complex criteria tests, not combinator, evaluateFieldLogic fix --- .../complexCriteria.test.ts | 1217 +++++++++++++++++ .../criteriaCompletionChecker.ts | 17 + 2 files changed, 1234 insertions(+) create mode 100644 src/anonymousUserTracking/complexCriteria.test.ts diff --git a/src/anonymousUserTracking/complexCriteria.test.ts b/src/anonymousUserTracking/complexCriteria.test.ts new file mode 100644 index 00000000..c7284341 --- /dev/null +++ b/src/anonymousUserTracking/complexCriteria.test.ts @@ -0,0 +1,1217 @@ +import { SHARED_PREFS_EVENT_LIST_KEY } from '../constants'; +import CriteriaCompletionChecker from './criteriaCompletionChecker'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +describe('complexCriteria', () => { + beforeEach(() => { + (global as any).localStorage = localStorageMock; + }); + + // complex criteria + it('should return criteriaId 98 (complex criteria 1)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [ + { + id: '12', + name: 'monitor', + price: 50, + quantity: 10 + } + ], + total: 50, + eventType: 'purchase' + }, + { + dataFields: { + preferred_car_models: 'Honda', + country: 'Japan' + }, + eventType: 'user' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '98', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'eventName', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 23, + value: 'button.clicked' + }, + { + field: 'button-clicked.animal', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 25, + value: 'giraffe' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'updateCart.updatedShoppingCartItems.price', + fieldType: 'double', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 28, + value: '120' + }, + { + field: + 'updateCart.updatedShoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 29, + valueLong: 100, + value: '100' + } + ] + } + } + ] + }, + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'purchase', + id: 31, + value: 'monitor' + }, + { + field: 'shoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'purchase', + id: 32, + valueLong: 5, + value: '5' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'user', + id: 34, + value: 'Japan' + }, + { + field: 'preferred_car_models', + fieldType: 'string', + comparatorType: 'Contains', + dataType: 'user', + id: 36, + value: 'Honda' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('98'); + }); + + it('should return null (complex criteria 1 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [ + { + id: '12', + name: 'monitor', + price: 50, + quantity: 10 + } + ], + total: 50, + eventType: 'purchase' + }, + { + dataFields: { + preferred_car_models: 'Honda' + }, + eventType: 'user' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '98', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'eventName', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 23, + value: 'button.clicked' + }, + { + field: 'button-clicked.animal', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 25, + value: 'giraffe' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'updateCart.updatedShoppingCartItems.price', + fieldType: 'double', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 28, + value: '120' + }, + { + field: + 'updateCart.updatedShoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 29, + valueLong: 100, + value: '100' + } + ] + } + } + ] + }, + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'purchase', + id: 31, + value: 'monitor' + }, + { + field: 'shoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'purchase', + id: 32, + valueLong: 5, + value: '5' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'user', + id: 34, + value: 'Japan' + }, + { + field: 'preferred_car_models', + fieldType: 'string', + comparatorType: 'Contains', + dataType: 'user', + id: 36, + value: 'Honda' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 99 (complex criteria 2)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ id: '12', name: 'Mocha', price: 90, quantity: 50 }], + total: 50, + eventType: 'cartUpdate' + }, + { + items: [{ id: '12', name: 'Mocha', price: 90, quantity: 50 }], + total: 50, + eventType: 'cartUpdate' + }, + { + dataFields: { + preferred_car_models: 'Subaru', + country: 'USA' + }, + eventType: 'user' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '99', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'eventName', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 2, + value: 'button-clicked' + }, + { + field: 'button-clicked.lastPageViewed', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 4, + value: 'welcome page' + } + ] + } + }, + { + dataType: 'customEvent', + minMatch: 2, + maxMatch: 3, + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'updateCart.updatedShoppingCartItems.price', + fieldType: 'double', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 6, + value: '85' + }, + { + field: + 'updateCart.updatedShoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 7, + valueLong: 50, + value: '50' + } + ] + } + } + ] + }, + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'purchase', + id: 16, + isFiltering: false, + value: 'coffee' + }, + { + field: 'shoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'purchase', + id: 17, + valueLong: 2, + value: '2' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'user', + id: 19, + value: 'USA' + }, + { + field: 'preferred_car_models', + fieldType: 'string', + comparatorType: 'Contains', + dataType: 'user', + id: 21, + value: 'Subaru' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('99'); + }); + + it('should return null (complex criteria 2 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ id: '12', name: 'Mocha', price: 90, quantity: 50 }], + total: 50, + eventType: 'cartUpdate' + }, + { + dataFields: { + preferred_car_models: 'Subaru', + country: 'USA' + }, + eventType: 'user' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '99', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'eventName', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 2, + value: 'button-clicked' + }, + { + field: 'button-clicked.lastPageViewed', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 4, + value: 'welcome page' + } + ] + } + }, + { + dataType: 'customEvent', + minMatch: 2, + maxMatch: 3, + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'updateCart.updatedShoppingCartItems.price', + fieldType: 'double', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 6, + value: '85' + }, + { + field: + 'updateCart.updatedShoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 7, + valueLong: 50, + value: '50' + } + ] + } + } + ] + }, + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'purchase', + id: 16, + isFiltering: false, + value: 'coffee' + }, + { + field: 'shoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'purchase', + id: 17, + valueLong: 2, + value: '2' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'user', + id: 19, + value: 'USA' + }, + { + field: 'preferred_car_models', + fieldType: 'string', + comparatorType: 'Contains', + dataType: 'user', + id: 21, + value: 'Subaru' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 100 (complex criteria 3)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ id: '12', name: 'Mocha', price: 90, quantity: 50 }], + total: 50, + eventType: 'cartUpdate' + }, + { + items: [{ id: '12', name: 'Mocha', price: 90, quantity: 50 }], + total: 50, + eventType: 'cartUpdate' + }, + + { + dataFields: { + preferred_car_models: 'Subaru', + country: 'USA' + }, + eventType: 'user' + }, + { + eventName: 'button-clicked', + dataFields: { + 'button-clicked.lastPageViewed': 'welcome page' + }, + eventType: 'customEvent' + }, + { + items: [ + { + id: '12', + name: 'coffee', + price: 10, + quantity: 5 + } + ], + total: 2, + eventType: 'purchase' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '100', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'eventName', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 2, + value: 'button-clicked' + }, + { + field: 'button-clicked.lastPageViewed', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 4, + value: 'welcome page' + } + ] + } + }, + { + dataType: 'customEvent', + minMatch: 2, + maxMatch: 3, + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'updateCart.updatedShoppingCartItems.price', + fieldType: 'double', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 6, + value: '85' + }, + { + field: + 'updateCart.updatedShoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 7, + valueLong: 50, + value: '50' + } + ] + } + }, + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'purchase', + id: 9, + value: 'coffee' + }, + { + field: 'shoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'purchase', + id: 10, + valueLong: 2, + value: '2' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'user', + id: 12, + value: 'USA' + }, + { + field: 'preferred_car_models', + fieldType: 'string', + comparatorType: 'Contains', + dataType: 'user', + id: 14, + value: 'Subaru' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('100'); + }); + + it('should return null (complex criteria 3 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ id: '12', name: 'Mocha', price: 90, quantity: 50 }], + total: 50, + eventType: 'cartUpdate' + }, + { + items: [{ id: '12', name: 'Mocha', price: 90, quantity: 50 }], + total: 50, + eventType: 'cartUpdate' + }, + + { + dataFields: { + preferred_car_models: 'Subaru', + country: 'USA' + }, + eventType: 'user' + }, + { + eventName: 'button-clicked', + dataFields: { + 'button-clicked.lastPageViewed': 'welcome page' + }, + eventType: 'customEvent' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '100', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'eventName', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 2, + value: 'button-clicked' + }, + { + field: 'button-clicked.lastPageViewed', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 4, + value: 'welcome page' + } + ] + } + }, + { + dataType: 'customEvent', + minMatch: 2, + maxMatch: 3, + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'updateCart.updatedShoppingCartItems.price', + fieldType: 'double', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 6, + value: '85' + }, + { + field: + 'updateCart.updatedShoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'customEvent', + id: 7, + valueLong: 50, + value: '50' + } + ] + } + }, + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'purchase', + id: 9, + value: 'coffee' + }, + { + field: 'shoppingCartItems.quantity', + fieldType: 'long', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'purchase', + id: 10, + valueLong: 2, + value: '2' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'user', + id: 12, + value: 'USA' + }, + { + field: 'preferred_car_models', + fieldType: 'string', + comparatorType: 'Contains', + dataType: 'user', + id: 14, + value: 'Subaru' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 101 (complex criteria 4)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [ + { id: '12', name: 'sneakers', price: 10, quantity: 5 }, + { id: '13', name: 'slippers', price: 10, quantity: 3 } + ], + total: 2, + eventType: 'purchase' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '101', + name: 'Complex Criteria 4: (NOT 9) AND 10', + createdAt: 1719328083918, + updatedAt: 1719328083918, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'sneakers', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.quantity', + comparatorType: 'LessThanOrEqualTo', + value: '3', + fieldType: 'long' + } + ] + } + } + ] + }, + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'slippers', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.quantity', + comparatorType: 'GreaterThanOrEqualTo', + value: '3', + fieldType: 'long' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('101'); + }); + + it('should return null (complex criteria 4 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [ + { id: '12', name: 'sneakers', price: 10, quantity: 2 }, + { id: '13', name: 'slippers', price: 10, quantity: 3 } + ], + total: 2, + eventType: 'purchase' + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criterias: [ + { + criteriaId: '101', + name: 'Complex Criteria 4: (NOT 9) AND 10', + createdAt: 1719328083918, + updatedAt: 1719328083918, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'sneakers', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.quantity', + comparatorType: 'LessThanOrEqualTo', + value: '3', + fieldType: 'long' + } + ] + } + } + ] + }, + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'slippers', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.quantity', + comparatorType: 'GreaterThanOrEqualTo', + value: '3', + fieldType: 'long' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); +}); diff --git a/src/anonymousUserTracking/criteriaCompletionChecker.ts b/src/anonymousUserTracking/criteriaCompletionChecker.ts index b16427b2..649b7d3b 100644 --- a/src/anonymousUserTracking/criteriaCompletionChecker.ts +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -200,6 +200,14 @@ class CriteriaCompletionChecker { } } return false; + } else if (combinator === 'Not') { + for (let i = 0; i < searchQueries.length; i++) { + searchQueries[i]['isNot'] = true; + if (this.evaluateTree(searchQueries[i], localEventData)) { + return false; + } + } + return true; } } else if (node.searchCombo) { return this.evaluateSearchQueries(node, localEventData); @@ -223,6 +231,7 @@ class CriteriaCompletionChecker { const searchCombo = node.searchCombo; const searchQueries = searchCombo?.searchQueries || []; const combinator = searchCombo?.combinator || ''; + const isNot = Object.prototype.hasOwnProperty.call(node, 'isNot'); if (this.evaluateEvent(eventData, searchQueries, combinator)) { if (node.minMatch) { const minMatch = node.minMatch - 1; @@ -231,7 +240,12 @@ class CriteriaCompletionChecker { continue; } } + if (isNot && !(i + 1 === localEventData.length)) { + continue; + } return true; + } else if (isNot) { + return false; } } } @@ -298,6 +312,9 @@ class CriteriaCompletionChecker { !searchQuery.field.startsWith(UPDATECART_ITEM_PREFIX) && !searchQuery.field.startsWith(PURCHASE_ITEM_PREFIX) ); + if (filteredSearchQueries.length === 0) { + return itemMatchedResult; + } const matchResult = filteredSearchQueries.every((query: any) => { const field = query.field; const eventKeyItems = filteredLocalDataKeys.filter(