diff --git a/AnonymousUserEventTracking.md b/AnonymousUserEventTracking.md new file mode 100644 index 00000000..72a99b2f --- /dev/null +++ b/AnonymousUserEventTracking.md @@ -0,0 +1,138 @@ +# Anonymous User Event Tracking Iterable's Web SDK +The Anonymous User Tracking Module is a pivotal component within our WEB SDK that seamlessly captures user events while maintaining anonymity for non-logged-in users. This module is designed to diligently gather and store events triggered by users who have not yet signed in. Once specific criteria are met or when the user decides to engage further, the module securely synchronizes all accumulated anonymous events with our servers. + +By adopting a privacy-first approach, the module allows us to gain valuable insights into user interactions without compromising their personal information. As users transition from anonymous to logged-in status, the module smoothly transitions as well, associating future events with the user's unique identity while ensuring the continuity of event tracking. + +Key Features: + +- Anonymous Event Tracking: Captures a diverse range of user events even before they log in, ensuring a comprehensive understanding of user behavior. +Privacy Protection: Safeguards user anonymity by collecting and storing events without requiring personal information. +- Event Synchronization: Upon meeting predefined conditions, securely transmits all anonymous events to the server, enabling data-driven decision-making. +- Seamless User Transition: Effortlessly shifts from tracking anonymous events to associating events with specific users as they log in. +- Enhanced Insights: Provides a holistic view of user engagement patterns, contributing to a more informed product optimization strategy. +Implementing the Anonymous + +# Installation + +To install this SDK through NPM: + +``` +$ npm install @iterable/web-sdk +``` + +with yarn: + +``` +$ yarn add @iterable/web-sdk +``` + +or with a CDN: + +```js + +``` + +# Methods +- [`getAnonCriteria`] +- [`trackAnonEvent`] +- [`trackAnonPurchaseEvent`] +- [`trackAnonUpdateCart`] +- [`createUser`] +- [`syncEvents`] +- [`checkCriteriaCompletion`] + +# Usage + +1. `trackAnonEvent` +The 'trackAnonEvent' function within the Iterable-Web SDK empowers seamless tracking of diverse web events. Developers can enrich event data with specific metadata using the 'dataFields' attribute. This function intelligently distinguishes between logged-in and non-logged-in users, securely storing event data on the server post-login, while locally preserving data for anonymous users, ensuring comprehensive event monitoring in both scenarios. + +```ts +const eventDetails = { + ...conditionalParams, + createNewFields: true, + createdAt: (Date.now() / 1000) | 0, + dataFields: { website: { domain: 'omni.com' }, eventType: 'track' }, +}; + +await anonymousUserEventManager.trackAnonEvent(eventDetails); +``` + +2. `trackAnonPurchaseEvent` +The 'trackAnonPurchaseEvent' function in the Iterable-Web SDK enables precise tracking of purchase-related web events. Developers can seamlessly include specific details about purchased items. With an innate understanding of user authentication status, the function securely stores event data on the server post-login, while also providing localized storage for non-logged-in users, guaranteeing comprehensive event monitoring in both usage scenarios. + +```ts +const eventDetails = { + ...conditionalParams, + items: [{ name: purchaseItem, id: 'fdsafds', price: 100, quantity: 2 }], + total: 200 +} + +await anonymousUserEventManager.trackAnonPurchaseEvent(eventDetails); +``` + +3. `trackAnonUpdateCart` +The 'trackAnonUpdateCart' function in the Iterable-Web SDK empowers effortless tracking of web events related to cart updates. Developers can accurately outline details for multiple items within the cart. It seamlessly handles data, securely transmitting events to the server upon user login, while also providing local storage for event details in the absence of user login, ensuring comprehensive event tracking in all scenarios. + +```ts +const eventDetails = { + ...conditionalParams, + items: [{ name: cartItem, id: 'fdsafds', price: 100, quantity: 2 }] +} + +await anonymousUserEventManager.trackAnonUpdateCart(eventDetails); +``` + +4. `createUser` +The 'createUser' function in the Iterable-Web SDK facilitates user creation by assigning a unique user UUID. This function also supports the seamless updating of user details on the server, providing a comprehensive solution for managing user data within your application. + +```ts +await anonymousUserEventManager.createUser(uuid, process.env.API_KEY); +``` + +5. `getAnonCriteria` +The 'getAnonCriteria' function within the Iterable-Web SDK retrieves criteria from the server for matching purposes. It efficiently fetches and returns an array of criteria, providing developers with essential tools to enhance their application's functionality through data-driven decision-making. + +```ts +const criteriaList = await anonymousUserEventManager.getAnonCriteria(); +``` + +6. `checkCriteriaCompletion` +The 'checkCriteriaCompletion' function in the Iterable-Web SDK performs a local assessment of stored events to determine if they fulfill specific criteria. If any of the stored events satisfy the criteria, the function returns 'true', offering developers a reliable method to validate the completion status of predefined conditions based on accumulated event data. + +```ts +const isCriteriaCompleted = await anonymousUserEventManager.checkCriteriaCompletion(); +``` + +7. `syncEvents` +The 'syncEvents' function within the Iterable-Web SDK facilitates the seamless synchronization of locally stored events to the server while sequentially maintaining their order. This function efficiently transfers all accumulated events, clearing the local storage in the process, ensuring data consistency and integrity between the client and server-side environments. + +```ts +await anonymousUserEventManager.syncEvents(); +``` + +# Example + +```ts +const eventDetails = { + ...conditionalParams, + createNewFields: true, + createdAt: (Date.now() / 1000) | 0, + userId: loggedInUser, + dataFields: { website: { domain: 'omni.com' }, eventType: 'track' }, + deviceInfo: { + appPackageName: 'my-website' + } +}; + +await anonymousUserEventManager.trackAnonEvent(eventDetails); +const isCriteriaCompleted = await anonymousUserEventManager.checkCriteriaCompletion(); + +if (isCriteriaCompleted) { + const userId = uuidv4(); + const App = await initialize(process.env.API_KEY); + await App.setUserID(userId); + await anonymousUserEventManager.createUser(userId, process.env.API_KEY); + setLoggedInUser({ type: 'user_update', data: userId }); + await anonymousUserEventManager.syncEvents(); +} +``` diff --git a/README.md b/README.md index 7f39c290..74f86699 100644 --- a/README.md +++ b/README.md @@ -2343,20 +2343,17 @@ At that point, further requests to Iterable's API will fail. To perform a manual JWT token refresh, call [`refreshJwtToken`](#refreshjwttoken). -# Iterable's European data center (EUDC) +# Iterable's European data center (EDC) -If your Iterable project is hosted on Iterable's [European data center (EUDC)](https://support.iterable.com/hc/articles/17572750887444), +If your Iterable project is hosted on Iterable's [European data center (EDC)](https://support.iterable.com/hc/articles/17572750887444), you'll need to configure Iterable's Web SDK to interact with Iterable's EU-based API endpoints. -To do this, you have two options: +To do this: -- On the web server that hosts your site, set the `IS_EU_ITERABLE_SERVICE` - environment variable to `true`. - -- Or, when use [`initializeWithConfig`](#initializeWithConfig) to initialize - the SDK (rather then [`initialize`](#initialize)), and set set the - `isEuIterableService` configuration option to `true`. For example: +- Use [`initializeWithConfig`](#initializeWithConfig) to initialize the SDK + (rather then [`initialize`](#initialize)). +- Set the `isEuIterableService` configuration option to `true`. For example: ```ts import { initializeWithConfig } from '@iterable/web-sdk'; diff --git a/example/yarn.lock b/example/yarn.lock index 464f77f3..5a9b6554 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1470,12 +1470,19 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browser-process-hrtime@^1.0.0: version "1.0.0" @@ -2379,10 +2386,17 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -3592,11 +3606,11 @@ methods@~1.1.2: integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.51.0: diff --git a/package.json b/package.json index 28ea079e..9910f008 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@iterable/web-sdk", "description": "Iterable SDK for JavaScript and Node.", - "version": "1.1.1", + "version": "1.1.2", "homepage": "https://iterable.com/", "repository": { "type": "git", @@ -73,7 +73,7 @@ "@types/jest": "^27.0.2", "@types/node": "^12.7.1", "@types/throttle-debounce": "^2.1.0", - "@types/uuid": "^9.0.2", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/parser": "^5.38.1", "@webpack-cli/serve": "^1.6.0", diff --git a/react-example/.eslintrc b/react-example/.eslintrc index 1583a522..1f749a6a 100644 --- a/react-example/.eslintrc +++ b/react-example/.eslintrc @@ -1,13 +1,8 @@ { - "extends": [ - "../.eslintrc", - "plugin:react/recommended" - ], + "extends": ["../.eslintrc", "plugin:react/recommended"], "rules": { "@typescript-eslint/no-empty-interface": "off", - "react/react-in-jsx-scope": "off", + "react/react-in-jsx-scope": "off" }, - "ignorePatterns": [ - "node_modules/" - ] -} \ No newline at end of file + "ignorePatterns": ["node_modules/"] +} diff --git a/react-example/src/components/EventsForm.tsx b/react-example/src/components/EventsForm.tsx index 7bf6650f..4a113612 100644 --- a/react-example/src/components/EventsForm.tsx +++ b/react-example/src/components/EventsForm.tsx @@ -28,10 +28,9 @@ export const EventsForm: FC = ({ ); const [trackEvent, setTrackEvent] = useState(''); - const [isTrackingEvent, setTrackingEvent] = useState(false); - const handleTrack = (e: FormEvent) => { + const handleTrack = async (e: FormEvent) => { e.preventDefault(); setTrackingEvent(true); @@ -50,7 +49,6 @@ export const EventsForm: FC = ({ }) .catch((e: any) => { setTrackResponse(JSON.stringify(e.response.data)); - setTrackingEvent(false); }); }; diff --git a/react-example/src/components/LoginFormWithoutJWT.tsx b/react-example/src/components/LoginFormWithoutJWT.tsx new file mode 100644 index 00000000..5fd49ca7 --- /dev/null +++ b/react-example/src/components/LoginFormWithoutJWT.tsx @@ -0,0 +1,150 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { ChangeEvent, FC, FormEvent, useState } from 'react'; +import styled from 'styled-components'; + +import { TextField as _TextField } from './TextField'; +import { Button as _Button } from './Button'; + +import { useUser } from '../context/Users'; + +const TextField = styled(_TextField)``; + +const Button = styled(_Button)` + margin-left: 0.4em; + max-width: 425px; +`; + +const Form = styled.form` + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-end; + height: 100%; + + ${TextField} { + align-self: stretch; + margin-top: 5px; + } +`; + +const StyledDiv = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; +`; + +const Error = styled.div` + color: red; +`; + +interface Props { + setEmail: (email: string) => Promise; + setUserId: (userId: string, merge?: boolean) => Promise; + logout: () => void; +} + +export const LoginFormWithoutJWT: FC = ({ + setEmail, + setUserId, + logout +}) => { + const [useEmail, setUseEmail] = useState(true); + const [user, updateUser] = useState(process.env.LOGIN_EMAIL || ''); + + const [error, setError] = useState(''); + + const [isEditingUser, setEditingUser] = useState(false); + + const { loggedInUser, setLoggedInUser } = useUser(); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + const setUser = useEmail ? setEmail : setUserId; + + setUser(user, true) + .then(() => { + setEditingUser(false); + setLoggedInUser({ type: 'user_update', data: user }); + }) + .catch(() => setError('Something went wrong!')); + }; + + const handleLogout = () => { + logout(); + setLoggedInUser({ type: 'user_update', data: '' }); + }; + + const handleEditUser = () => { + updateUser(loggedInUser); + setEditingUser(true); + }; + + const handleCancelEditUser = () => { + updateUser(''); + setEditingUser(false); + }; + + const handleRadioChange = (e: ChangeEvent) => { + setUseEmail(e.target.value === 'email'); + }; + + const first5 = loggedInUser.substring(0, 5); + const last9 = loggedInUser.substring(loggedInUser.length - 9); + + return ( + <> + {loggedInUser && !isEditingUser ? ( + <> + + + + ) : ( + +
+
+ + +
+
+ + +
+
+
+ updateUser(e.target.value)} + value={user} + placeholder="e.g. hello@gmail.com" + required + data-qa-login-input + /> + + {isEditingUser && ( + + )} + + {error && {error}} +
+ )} + + ); +}; + +export default LoginFormWithoutJWT; diff --git a/react-example/src/index.tsx b/react-example/src/index.tsx index 36d698eb..4767eff4 100644 --- a/react-example/src/index.tsx +++ b/react-example/src/index.tsx @@ -14,6 +14,7 @@ import { EmbeddedMessage } from './views/Embedded'; import { Link } from './components/Link'; import { LoginForm } from './components/LoginForm'; import { EmbeddedMsgs } from './views/EmbeddedMsgs'; +import AUTTesting from './views/AUTTesting'; import { UserProvider } from './context/Users'; import { EmbeddedMsgsImpressionTracker } from './views/EmbeddedMsgsImpressionTracker'; @@ -45,7 +46,8 @@ const HomeLink = styled(Link)` authToken: process.env.API_KEY || '', configOptions: { isEuIterableService: false, - dangerouslyAllowJsPopups: true + dangerouslyAllowJsPopups: true, + enableAnonTracking: true }, generateJWT: ({ email, userID }) => axios @@ -65,8 +67,16 @@ const HomeLink = styled(Link)` ) .then((response: any) => response.data?.token) }; - const { setEmail, setUserID, logout, refreshJwtToken } = - initializeWithConfig(initializeParams); + const { + setEmail, + setUserID, + logout, + refreshJwtToken, + toggleAnonUserTrackingConsent + } = initializeWithConfig(initializeParams); + + const handleConsent = (consent?: boolean) => + toggleAnonUserTrackingConsent(consent); const container = document.getElementById('root'); const root = createRoot(container); @@ -98,6 +108,10 @@ const HomeLink = styled(Link)` path="/embedded-msgs-impression-tracker" element={} /> + } + /> diff --git a/react-example/src/indexWithoutJWT.tsx b/react-example/src/indexWithoutJWT.tsx new file mode 100644 index 00000000..bdedcbc7 --- /dev/null +++ b/react-example/src/indexWithoutJWT.tsx @@ -0,0 +1,101 @@ +import { initializeWithConfig, WithoutJWTParams } from '@iterable/web-sdk'; +import ReactDOM from 'react-dom'; +import './styles/index.css'; + +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import styled from 'styled-components'; +import { Home } from './views/Home'; +import { Commerce } from './views/Commerce'; +import { Events } from './views/Events'; +import { Users } from './views/Users'; +import { InApp } from './views/InApp'; +import LoginFormWithoutJWT from './components/LoginFormWithoutJWT'; +import AUTTesting from './views/AUTTesting'; +import { EmbeddedMsgs } from './views/EmbeddedMsgs'; +import { EmbeddedMessage } from './views/Embedded'; +import { EmbeddedMsgsImpressionTracker } from './views/EmbeddedMsgsImpressionTracker'; +import { Link } from './components/Link'; +import { UserProvider } from './context/Users'; + +const Wrapper = styled.div` + display: flex; + flex-flow: column; +`; + +const RouteWrapper = styled.div` + width: 90%; + margin: 0 auto; +`; + +const HeaderWrapper = styled.div` + display: flex; + flex-flow: row; + align-items: center; + justify-content: space-between; + margin: 1em; +`; + +const HomeLink = styled(Link)` + width: 100px; +`; + +((): void => { + // Here we are testing it using NON-JWT based project. + const initializeParams: WithoutJWTParams = { + authToken: process.env.API_KEY || '', + configOptions: { + isEuIterableService: false, + dangerouslyAllowJsPopups: true, + enableAnonTracking: true, + onAnonUserCreated: (userId: string) => { + console.log('onAnonUserCreated', userId); + } + } + }; + + const { setUserID, logout, setEmail, toggleAnonUserTrackingConsent } = + initializeWithConfig(initializeParams); + + const handleConsent = (consent?: boolean) => + toggleAnonUserTrackingConsent(consent); + + // eslint-disable-next-line react/no-deprecated + ReactDOM.render( + + + + + + Home + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } + /> + + + + + , + document.getElementById('root') + ); +})(); diff --git a/react-example/src/styles/index.css b/react-example/src/styles/index.css index 28642755..e6033d05 100644 --- a/react-example/src/styles/index.css +++ b/react-example/src/styles/index.css @@ -1,4 +1,5 @@ -html, body { +html, +body { margin: 0; padding: 0; } @@ -30,7 +31,7 @@ html, body { } #change-email-form input { - margin-top: .5em; + margin-top: 0.5em; flex-grow: 1; padding: 1em; } @@ -42,17 +43,17 @@ html, body { flex-flow: column; justify-content: center; } - + .input-wrapper { margin-right: 0; transform: translateY(0); } - + #change-email-form button { width: 100%; margin-top: 1em; } - + #change-email-form input { height: 50px; } @@ -62,4 +63,34 @@ footer { display: flex; justify-content: flex-end; align-items: flex-end; -} \ No newline at end of file +} + +#cookie-consent-container { + display: flex; + justify-content: center; + flex-direction: column; + + position: fixed; + bottom: 0; + right: 0; + + box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.2); + padding: 1em; + background: #fff; + margin: 1em; + max-width: 400px; + + h3 { + margin-top: 0; + margin-bottom: 0.5em; + } + + p { + margin-top: 0; + } + + div { + display: flex; + gap: 0.5em; + } +} diff --git a/react-example/src/views/AUTTesting.tsx b/react-example/src/views/AUTTesting.tsx new file mode 100644 index 00000000..e0a0ab2c --- /dev/null +++ b/react-example/src/views/AUTTesting.tsx @@ -0,0 +1,304 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { FC, FormEvent, useState } from 'react'; +import { + updateCart, + trackPurchase, + UpdateCartRequestParams, + TrackPurchaseRequestParams, + updateUser, + UpdateUserParams, + track, + InAppTrackRequestParams +} from '@iterable/web-sdk'; +import { TextField } from '../components/TextField'; +import { + Button, + EndpointWrapper, + Form, + Heading, + Response +} from './Components.styled'; + +interface Props { + setConsent?: (accept: boolean) => void; +} + +export const AUTTesting: FC = ({ setConsent }) => { + const [updateCartResponse, setUpdateCartResponse] = useState( + 'Endpoint JSON goes here' + ); + const [trackPurchaseResponse, setTrackPurchaseResponse] = useState( + 'Endpoint JSON goes here' + ); + + const [cartItem, setCartItem] = useState( + '{"items":[{"name":"piano","id":"fdsafds","price":100,"quantity":2}, {"name":"piano2","id":"fdsafds2","price":100,"quantity":5}]}' + ); + + const [purchaseItem, setPurchaseItem] = useState( + '{"items":[{"name":"Black Coffee","id":"fdsafds","price":100,"quantity":2}], "total": 100}' + ); + + const [isUpdatingCart, setUpdatingCart] = useState(false); + const [isTrackingPurchase, setTrackingPurchase] = useState(false); + const [userDataField, setUserDataField] = useState( + ' { "dataFields": {"email": "user@example.com","furniture": [{"furnitureType": "Sofa","furnitureColor": "White","lengthInches": 40,"widthInches": 60},{"furnitureType": "Sofa","furnitureColor": "Gray","lengthInches": 20,"widthInches": 30}] }}' + ); + const [isUpdatingUser, setUpdatingUser] = useState(false); + const [updateUserResponse, setUpdateUserResponse] = useState( + 'Endpoint JSON goes here' + ); + + const [trackResponse, setTrackResponse] = useState( + 'Endpoint JSON goes here' + ); + + const eventInput = + '{"eventName":"button-clicked", "dataFields": {"browserVisit.website.domain":"https://mybrand.com/socks"}}'; + const [trackEvent, setTrackEvent] = useState(eventInput); + const [isTrackingEvent, setTrackingEvent] = useState(false); + + const handleParseJson = (isUpdateCartCalled: boolean) => { + try { + // Parse JSON and assert its type + if (isUpdateCartCalled) { + const parsedObject = JSON.parse(cartItem) as UpdateCartRequestParams; + return parsedObject; + } + const parsedObject = JSON.parse( + purchaseItem + ) as TrackPurchaseRequestParams; + return parsedObject; + } catch (error) { + if (isUpdateCartCalled) { + setUpdateCartResponse(JSON.stringify(error.message)); + } else setTrackPurchaseResponse(JSON.stringify(error.message)); + return error; + } + }; + + const handleParseUserJson = () => { + try { + // Parse JSON and assert its type + return JSON.parse(userDataField) as UpdateUserParams; + } catch (error) { + setUpdateUserResponse(JSON.stringify(error.message)); + return error; + } + }; + + const handleUpdateCart = (e: FormEvent) => { + e.preventDefault(); + const jsonObj: UpdateCartRequestParams = handleParseJson(true); + if (jsonObj) { + setUpdatingCart(true); + try { + updateCart(jsonObj) + .then((response: any) => { + setUpdateCartResponse(JSON.stringify(response.data)); + setUpdatingCart(false); + }) + .catch((e: any) => { + setUpdateCartResponse(JSON.stringify(e)); + setUpdatingCart(false); + }); + } catch (error) { + setUpdateCartResponse(JSON.stringify(error.message)); + setUpdatingCart(false); + } + } + }; + + const handleTrackPurchase = (e: FormEvent) => { + e.preventDefault(); + const jsonObj: TrackPurchaseRequestParams = handleParseJson(false); + if (jsonObj) { + setTrackingPurchase(true); + try { + trackPurchase(jsonObj) + .then((response: any) => { + setTrackingPurchase(false); + setTrackPurchaseResponse(JSON.stringify(response.data)); + }) + .catch((e: any) => { + setTrackingPurchase(false); + setTrackPurchaseResponse(JSON.stringify(e)); + }); + } catch (error) { + setTrackingPurchase(false); + setTrackPurchaseResponse(JSON.stringify(error.message)); + } + } + }; + + const handleUpdateUser = (e: FormEvent) => { + e.preventDefault(); + const jsonObj = handleParseUserJson(); + if (jsonObj) { + setUpdatingUser(true); + try { + updateUser(jsonObj) + .then((response: any) => { + setUpdateUserResponse(JSON.stringify(response.data)); + setUpdatingUser(false); + }) + .catch((e: any) => { + setUpdateUserResponse(JSON.stringify(e)); + setUpdatingUser(false); + }); + } catch (error) { + setUpdateUserResponse(JSON.stringify(error.message)); + setUpdatingUser(false); + } + } + }; + + const handleParseTrackJson = () => { + try { + // Parse JSON and assert its type + const parsedObject = JSON.parse(trackEvent) as InAppTrackRequestParams; + return parsedObject; + } catch (error) { + setTrackResponse(JSON.stringify(error.message)); + return error; + } + }; + + const handleTrack = async (e: FormEvent) => { + e.preventDefault(); + setTrackingEvent(true); + + const jsonObj = handleParseTrackJson(); + if (jsonObj) { + const conditionalParams = jsonObj; + + try { + track({ + ...conditionalParams, + deviceInfo: { + appPackageName: 'my-website' + } + }) + .then((response: any) => { + setTrackResponse(JSON.stringify(response.data)); + setTrackingEvent(false); + }) + .catch((e: any) => { + if (e && e.response && e.response.data) { + setTrackResponse(JSON.stringify(e.response.data)); + } else { + setTrackResponse(JSON.stringify(e)); + } + setTrackingEvent(false); + }); + } catch (error) { + setTrackResponse(JSON.stringify(error.message)); + setTrackingEvent(false); + } + } + }; + + const formAttr = { 'data-qa-track-submit': true }; + const inputAttr = { 'data-qa-track-input': true }; + const responseAttr = { 'data-qa-track-response': true }; + + const acceptCookie = () => setConsent(true); + + const declineCookie = () => setConsent(false); + + const renderCookieConsent = setConsent && ( + + ); + + return ( + <> +

Commerce Endpoints

+ POST /updateCart + +
+ + setCartItem(e.target.value)} + id="item-1" + placeholder='e.g. {"items":[{"name":"piano","id":"fdsafds"}]}' + data-qa-cart-input + /> + + + {updateCartResponse} +
+ POST /trackPurchase + +
+ + setPurchaseItem(e.target.value)} + id="item-2" + placeholder='e.g. {"items":[{"id":"fdsafds","price":100}]}' + data-qa-purchase-input + /> + + + {trackPurchaseResponse} +
+

User Endpoint

+ POST /users/update + +
+ + setUserDataField(e.target.value)} + id="item-1" + placeholder="e.g. phone_number" + data-qa-update-user-input + required + /> + + + {updateUserResponse} +
+

Events Endpoint

+ POST /track + +
+ + setTrackEvent(e.target.value)} + id="item-1" + placeholder='e.g. {"eventName":"button-clicked"}' + {...inputAttr} + /> + + + {trackResponse} +
+ {renderCookieConsent} + + ); +}; + +export default AUTTesting; diff --git a/react-example/src/views/Commerce.tsx b/react-example/src/views/Commerce.tsx index f64c281c..74500397 100644 --- a/react-example/src/views/Commerce.tsx +++ b/react-example/src/views/Commerce.tsx @@ -28,34 +28,44 @@ export const Commerce: FC = () => { const handleUpdateCart = (e: FormEvent) => { e.preventDefault(); setUpdatingCart(true); - updateCart({ - items: [{ name: cartItem, id: 'fdsafds', price: 100, quantity: 2 }] - }) - .then((response: any) => { - setUpdateCartResponse(JSON.stringify(response.data)); - setUpdatingCart(false); + try { + updateCart({ + items: [{ name: cartItem, id: 'fdsafds', price: 100, quantity: 2 }] }) - .catch((e: any) => { - setUpdateCartResponse(JSON.stringify(e.response.data)); - setUpdatingCart(false); - }); + .then((response: any) => { + setUpdateCartResponse(JSON.stringify(response.data)); + setUpdatingCart(false); + }) + .catch((e: any) => { + setUpdateCartResponse(JSON.stringify(e.response.data)); + setUpdatingCart(false); + }); + } catch (error) { + setUpdateCartResponse(JSON.stringify(error.message)); + setUpdatingCart(false); + } }; const handleTrackPurchase = (e: FormEvent) => { e.preventDefault(); setTrackingPurchase(true); - trackPurchase({ - items: [{ name: purchaseItem, id: 'fdsafds', price: 100, quantity: 2 }], - total: 200 - }) - .then((response: any) => { - setTrackingPurchase(false); - setTrackPurchaseResponse(JSON.stringify(response.data)); + try { + trackPurchase({ + items: [{ name: purchaseItem, id: 'fdsafds', price: 100, quantity: 2 }], + total: 200 }) - .catch((e: any) => { - setTrackingPurchase(false); - setTrackPurchaseResponse(JSON.stringify(e.response.data)); - }); + .then((response: any) => { + setTrackingPurchase(false); + setTrackPurchaseResponse(JSON.stringify(response.data)); + }) + .catch((e: any) => { + setTrackingPurchase(false); + setTrackPurchaseResponse(JSON.stringify(e.response.data)); + }); + } catch (error) { + setTrackingPurchase(false); + setTrackPurchaseResponse(JSON.stringify(error.message)); + } }; return ( diff --git a/react-example/src/views/Components.styled.ts b/react-example/src/views/Components.styled.ts index e35d7959..0f2b9e58 100644 --- a/react-example/src/views/Components.styled.ts +++ b/react-example/src/views/Components.styled.ts @@ -40,3 +40,5 @@ export const Heading = styled.h2` export const StyledButton = styled(Button)` margin-top: 1em; `; + +export { Button }; diff --git a/react-example/src/views/Home.tsx b/react-example/src/views/Home.tsx index 749a0584..3c4d317c 100644 --- a/react-example/src/views/Home.tsx +++ b/react-example/src/views/Home.tsx @@ -41,6 +41,9 @@ export const Home: FC = () => ( Embedded msgs impressions tracker + + AUT Testing + ); diff --git a/react-example/src/views/Users.tsx b/react-example/src/views/Users.tsx index 4273d228..df990e36 100644 --- a/react-example/src/views/Users.tsx +++ b/react-example/src/views/Users.tsx @@ -40,17 +40,22 @@ export const Users: FC = () => { const handleUpdateUser = (e: FormEvent) => { e.preventDefault(); setUpdatingUser(true); - updateUser({ - dataFields: { [userDataField]: 'test-data' } - }) - .then((response: any) => { - setUpdateUserResponse(JSON.stringify(response.data)); - setUpdatingUser(false); + try { + updateUser({ + dataFields: { [userDataField]: 'test-data' } }) - .catch((e: any) => { - setUpdateUserResponse(JSON.stringify(e.response.data)); - setUpdatingUser(false); - }); + .then((response: any) => { + setUpdateUserResponse(JSON.stringify(response.data)); + setUpdatingUser(false); + }) + .catch((e: any) => { + setUpdateUserResponse(JSON.stringify(e.response.data)); + setUpdatingUser(false); + }); + } catch (error) { + setUpdateUserResponse(JSON.stringify(error.message)); + setUpdatingUser(false); + } }; const handleUpdateUserEmail = (e: FormEvent) => { diff --git a/react-example/yarn.lock b/react-example/yarn.lock index 9f69c545..47b17c56 100644 --- a/react-example/yarn.lock +++ b/react-example/yarn.lock @@ -552,11 +552,12 @@ integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== "@iterable/web-sdk@../": - version "1.1.1" + version "1.1.2" dependencies: "@pabra/sortby" "^1.0.1" "@types/ws" "8.5.4" axios "^1.6.2" + axios-mock-adapter "^1.22.0" buffer "^6.0.3" copy-webpack-plugin "^11.0.0" idb-keyval "^6.2.0" @@ -1606,6 +1607,14 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +axios-mock-adapter@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz#0f3e6be0fc9b55baab06f2d49c0b71157e7c053d" + integrity sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw== + dependencies: + fast-deep-equal "^3.1.3" + is-buffer "^2.0.5" + axios@^1.6.2, axios@^1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" @@ -3518,6 +3527,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" diff --git a/src/anonymousUserTracking/anonymousUserEventManager.ts b/src/anonymousUserTracking/anonymousUserEventManager.ts new file mode 100644 index 00000000..d55d48b3 --- /dev/null +++ b/src/anonymousUserTracking/anonymousUserEventManager.ts @@ -0,0 +1,441 @@ +/* eslint-disable class-methods-use-this */ +import { v4 as uuidv4 } from 'uuid'; +import { + UpdateCartRequestParams, + TrackPurchaseRequestParams +} from '../commerce/types'; + +import { + GET_CRITERIA_PATH, + KEY_EVENT_NAME, + KEY_CREATED_AT, + KEY_DATA_FIELDS, + KEY_CREATE_NEW_FIELDS, + SHARED_PREFS_EVENT_TYPE, + TRACK_EVENT, + SHARED_PREFS_EVENT_LIST_KEY, + KEY_ITEMS, + KEY_TOTAL, + TRACK_PURCHASE, + UPDATE_USER, + TRACK_UPDATE_CART, + SHARED_PREFS_CRITERIA, + SHARED_PREFS_ANON_SESSIONS, + ENDPOINT_TRACK_ANON_SESSION, + WEB_PLATFORM, + KEY_PREFER_USERID, + ENDPOINTS, + DEFAULT_EVENT_THRESHOLD_LIMIT, + SHARED_PREF_ANON_USAGE_TRACKED +} from '../constants'; +import { baseIterableRequest } from '../request'; +import { IterableResponse } from '../types'; +import CriteriaCompletionChecker from './criteriaCompletionChecker'; +import { TrackAnonSessionParams } from '../utils/types'; +import { UpdateUserParams } from '../users/types'; +import { trackSchema } from '../events/events.schema'; +import { + trackPurchaseSchema, + updateCartSchema +} from '../commerce/commerce.schema'; +import { updateUserSchema } from '../users/users.schema'; +import { InAppTrackRequestParams } from '../events'; +import config from '../utils/config'; + +type AnonUserFunction = (userId: string) => void; + +let anonUserIdSetter: AnonUserFunction | null = null; + +export function registerAnonUserIdSetter(setterFunction: AnonUserFunction) { + anonUserIdSetter = setterFunction; +} + +export function isAnonymousUsageTracked(): boolean { + const anonymousUsageTracked = localStorage.getItem( + SHARED_PREF_ANON_USAGE_TRACKED + ); + return anonymousUsageTracked === 'true'; +} + +export class AnonymousUserEventManager { + updateAnonSession() { + try { + const anonymousUsageTracked = isAnonymousUsageTracked(); + + if (!anonymousUsageTracked) return; + + const strAnonSessionInfo = localStorage.getItem( + SHARED_PREFS_ANON_SESSIONS + ); + let anonSessionInfo: { + itbl_anon_sessions?: { + number_of_sessions?: number; + first_session?: number; + last_session?: number; + }; + } = {}; + + if (strAnonSessionInfo) { + anonSessionInfo = JSON.parse(strAnonSessionInfo); + } + + // Update existing values or set them if they don't exist + anonSessionInfo.itbl_anon_sessions = + anonSessionInfo.itbl_anon_sessions || {}; + anonSessionInfo.itbl_anon_sessions.number_of_sessions = + (anonSessionInfo.itbl_anon_sessions.number_of_sessions || 0) + 1; + anonSessionInfo.itbl_anon_sessions.first_session = + anonSessionInfo.itbl_anon_sessions.first_session || + this.getCurrentTime(); + anonSessionInfo.itbl_anon_sessions.last_session = this.getCurrentTime(); + + // Update the structure to the desired format + const outputObject = { + itbl_anon_sessions: anonSessionInfo.itbl_anon_sessions + }; + + localStorage.setItem( + SHARED_PREFS_ANON_SESSIONS, + JSON.stringify(outputObject) + ); + } catch (error) { + console.error('Error updating anonymous session:', error); + } + } + + getAnonCriteria() { + const anonymousUsageTracked = isAnonymousUsageTracked(); + + if (!anonymousUsageTracked) return; + + baseIterableRequest({ + method: 'GET', + url: GET_CRITERIA_PATH, + data: {}, + validation: {} + }) + .then((response) => { + const criteriaData: any = response.data; + if (criteriaData) { + localStorage.setItem( + SHARED_PREFS_CRITERIA, + JSON.stringify(criteriaData) + ); + } + }) + .catch((e) => { + console.log('response', e); + }); + } + + async trackAnonEvent(payload: InAppTrackRequestParams) { + const newDataObject = { + [KEY_EVENT_NAME]: payload.eventName, + [KEY_CREATED_AT]: this.getCurrentTime(), + [KEY_DATA_FIELDS]: payload.dataFields, + [KEY_CREATE_NEW_FIELDS]: true, + [SHARED_PREFS_EVENT_TYPE]: TRACK_EVENT + }; + this.storeEventListToLocalStorage(newDataObject, false); + } + + async trackAnonUpdateUser(payload: UpdateUserParams) { + const newDataObject = { + ...payload.dataFields, + [SHARED_PREFS_EVENT_TYPE]: UPDATE_USER + }; + this.storeEventListToLocalStorage(newDataObject, true); + } + + async trackAnonPurchaseEvent(payload: TrackPurchaseRequestParams) { + const newDataObject = { + [KEY_ITEMS]: payload.items, + [KEY_CREATED_AT]: this.getCurrentTime(), + [KEY_DATA_FIELDS]: payload.dataFields, + [KEY_TOTAL]: payload.total, + [SHARED_PREFS_EVENT_TYPE]: TRACK_PURCHASE + }; + this.storeEventListToLocalStorage(newDataObject, false); + } + + async trackAnonUpdateCart(payload: UpdateCartRequestParams) { + const newDataObject = { + [KEY_ITEMS]: payload.items, + [SHARED_PREFS_EVENT_TYPE]: TRACK_UPDATE_CART, + [KEY_PREFER_USERID]: true, + [KEY_CREATED_AT]: this.getCurrentTime() + }; + this.storeEventListToLocalStorage(newDataObject, false); + } + + private checkCriteriaCompletion(): string | null { + const criteriaData = localStorage.getItem(SHARED_PREFS_CRITERIA); + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + try { + if (criteriaData && localStoredEventList) { + const checker = new CriteriaCompletionChecker(localStoredEventList); + return checker.getMatchedCriteria(criteriaData); + } + } catch (error) { + console.error('checkCriteriaCompletion', error); + } + + return null; + } + + private async createKnownUser(criteriaId: string) { + const anonymousUsageTracked = isAnonymousUsageTracked(); + + if (!anonymousUsageTracked) return; + + const userData = localStorage.getItem(SHARED_PREFS_ANON_SESSIONS); + const eventList = localStorage.getItem(SHARED_PREFS_EVENT_LIST_KEY); + const events = eventList ? JSON.parse(eventList) : []; + + const dataFields = { + ...events.find( + (event: any) => event[SHARED_PREFS_EVENT_TYPE] === UPDATE_USER + ) + }; + delete dataFields[SHARED_PREFS_EVENT_TYPE]; + + const userId = uuidv4(); + + if (userData) { + const userSessionInfo = JSON.parse(userData); + const userDataJson = userSessionInfo[SHARED_PREFS_ANON_SESSIONS]; + const payload: TrackAnonSessionParams = { + user: { + userId, + mergeNestedObjects: true, + createNewFields: true, + dataFields + }, + createdAt: this.getCurrentTime(), + deviceInfo: { + appPackageName: window.location.hostname, + deviceId: global.navigator.userAgent || '', + platform: WEB_PLATFORM + }, + anonSessionContext: { + totalAnonSessionCount: userDataJson.number_of_sessions, + lastAnonSession: userDataJson.last_session, + firstAnonSession: userDataJson.first_session, + matchedCriteriaId: parseInt(criteriaId, 10), + webPushOptIn: + this.getWebPushOptnIn() !== '' ? this.getWebPushOptnIn() : undefined + } + }; + const response = await baseIterableRequest({ + method: 'POST', + url: ENDPOINT_TRACK_ANON_SESSION, + data: payload + }).catch((e) => { + if (e?.response?.status === 409) { + this.getAnonCriteria(); + } + }); + if (response?.status === 200) { + // Update local storage, remove updateUser from local storage + localStorage.setItem( + SHARED_PREFS_EVENT_LIST_KEY, + JSON.stringify( + events.filter( + (event: any) => event[SHARED_PREFS_EVENT_TYPE] !== UPDATE_USER + ) + ) + ); + + const onAnonUserCreated = config.getConfig('onAnonUserCreated'); + + if (onAnonUserCreated) { + onAnonUserCreated(userId); + } + if (anonUserIdSetter !== null) { + await anonUserIdSetter(userId); + } + this.syncEvents(); + } + } + } + + async syncEvents() { + const strTrackEventList = localStorage.getItem(SHARED_PREFS_EVENT_LIST_KEY); + const trackEventList = strTrackEventList + ? JSON.parse(strTrackEventList) + : []; + + if (trackEventList.length) { + trackEventList.forEach( + ( + event: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ) => { + const eventType = event[SHARED_PREFS_EVENT_TYPE]; + // eslint-disable-next-line no-param-reassign + delete event.eventType; + switch (eventType) { + case TRACK_EVENT: { + this.track(event); + break; + } + case TRACK_PURCHASE: { + this.trackPurchase(event); + break; + } + case TRACK_UPDATE_CART: { + this.updateCart(event); + break; + } + case UPDATE_USER: { + this.updateUser({ dataFields: event }); + break; + } + default: + break; + } + this.removeAnonSessionCriteriaData(); + } + ); + } + } + + removeAnonSessionCriteriaData() { + localStorage.removeItem(SHARED_PREFS_ANON_SESSIONS); + localStorage.removeItem(SHARED_PREFS_EVENT_LIST_KEY); + } + + private async storeEventListToLocalStorage( + newDataObject: Record< + any /* eslint-disable-line @typescript-eslint/no-explicit-any */, + any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >, + shouldOverWrite: boolean + ) { + const anonymousUsageTracked = isAnonymousUsageTracked(); + + if (!anonymousUsageTracked) return; + + const strTrackEventList = localStorage.getItem(SHARED_PREFS_EVENT_LIST_KEY); + let previousDataArray = []; + + if (strTrackEventList) { + previousDataArray = JSON.parse(strTrackEventList); + } + + if (shouldOverWrite) { + const trackingType = newDataObject[SHARED_PREFS_EVENT_TYPE]; + const indexToUpdate = previousDataArray.findIndex( + (obj: any) => obj[SHARED_PREFS_EVENT_TYPE] === trackingType + ); + if (indexToUpdate !== -1) { + const dataToUpdate = previousDataArray[indexToUpdate]; + + previousDataArray[indexToUpdate] = { + ...dataToUpdate, + ...newDataObject + }; + } else { + previousDataArray.push(newDataObject); + } + } else { + previousDataArray.push(newDataObject); + } + + // - The code below limits the number of events stored in local storage. + // - The event list acts as a queue, with the oldest events being deleted + // when new events are stored once the event threshold limit is reached. + + const eventThresholdLimit = + (config.getConfig('eventThresholdLimit') as number) ?? + DEFAULT_EVENT_THRESHOLD_LIMIT; + if (previousDataArray.length > eventThresholdLimit) { + previousDataArray = previousDataArray.slice( + previousDataArray.length - eventThresholdLimit + ); + } + + localStorage.setItem( + SHARED_PREFS_EVENT_LIST_KEY, + JSON.stringify(previousDataArray) + ); + const criteriaId = this.checkCriteriaCompletion(); + if (criteriaId !== null) { + this.createKnownUser(criteriaId); + } + } + + private getCurrentTime = () => { + const dateInMillis = new Date().getTime(); + const dateInSeconds = Math.floor(dateInMillis / 1000); + return dateInSeconds; + }; + + private getWebPushOptnIn(): string { + const notificationManager = window.Notification; + if (notificationManager && notificationManager.permission === 'granted') { + return window.location.hostname; + } + return ''; + } + + track = (payload: InAppTrackRequestParams) => + baseIterableRequest({ + method: 'POST', + url: ENDPOINTS.event_track.route, + data: payload, + validation: { + data: trackSchema + } + }); + + updateCart = (payload: UpdateCartRequestParams) => + baseIterableRequest({ + method: 'POST', + url: ENDPOINTS.commerce_update_cart.route, + data: { + ...payload, + user: { + ...payload.user, + preferUserId: true + } + }, + validation: { + data: updateCartSchema + } + }); + + trackPurchase = (payload: TrackPurchaseRequestParams) => + baseIterableRequest({ + method: 'POST', + url: ENDPOINTS.commerce_track_purchase.route, + data: { + ...payload, + user: { + ...payload.user, + preferUserId: true + } + }, + validation: { + data: trackPurchaseSchema + } + }); + + updateUser = (payload: UpdateUserParams = {}) => { + if (payload.dataFields) { + return baseIterableRequest({ + method: 'POST', + url: ENDPOINTS.users_update.route, + data: { + ...payload, + preferUserId: true + }, + validation: { + data: updateUserSchema + } + }); + } + return null; + }; +} diff --git a/src/anonymousUserTracking/anonymousUserMerge.ts b/src/anonymousUserTracking/anonymousUserMerge.ts new file mode 100644 index 00000000..0f26f902 --- /dev/null +++ b/src/anonymousUserTracking/anonymousUserMerge.ts @@ -0,0 +1,48 @@ +/* eslint-disable class-methods-use-this */ +import { ENDPOINT_MERGE_USER } from '../constants'; +import { baseIterableRequest } from '../request'; +import { IterableResponse } from '../types'; + +export type MergeApiParams = { + sourceEmail: string | null; + sourceUserId: string | null; + destinationEmail: string | null; + destinationUserId: string | null; +}; + +export class AnonymousUserMerge { + mergeUser( + sourceUserId: string | null, + sourceEmail: string | null, + destinationUserId: string | null, + destinationEmail: string | null + ): Promise { + const mergeApiParams: MergeApiParams = { + sourceUserId, + sourceEmail, + destinationUserId, + destinationEmail + }; + return this.callMergeApi(mergeApiParams); + } + + private callMergeApi(data: MergeApiParams): Promise { + return new Promise((resolve, reject) => { + baseIterableRequest({ + method: 'POST', + url: ENDPOINT_MERGE_USER, + data + }) + .then((response) => { + if (response.status === 200) { + resolve(); + } else { + reject(new Error(`merge error: ${response.status}`)); // Reject if status is not 200 + } + }) + .catch((e) => { + reject(e); // Reject the promise if the request fails + }); + }); + } +} diff --git a/src/anonymousUserTracking/complexCriteria.test.ts b/src/anonymousUserTracking/complexCriteria.test.ts new file mode 100644 index 00000000..8d8eb7cd --- /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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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: { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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 new file mode 100644 index 00000000..59368e16 --- /dev/null +++ b/src/anonymousUserTracking/criteriaCompletionChecker.ts @@ -0,0 +1,607 @@ +/* eslint-disable class-methods-use-this */ +import { + SHARED_PREFS_EVENT_TYPE, + KEY_ITEMS, + TRACK_PURCHASE, + TRACK_UPDATE_CART, + TRACK_EVENT, + UPDATE_CART, + UPDATE_USER, + KEY_EVENT_NAME, + UPDATECART_ITEM_PREFIX, + PURCHASE_ITEM_PREFIX, + PURCHASE_ITEM +} from '../constants'; + +interface SearchQuery { + combinator: string; + /* eslint-disable-next-line no-use-before-define */ + searchQueries: SearchQuery[] | Criteria[]; + dataType?: string; + searchCombo?: SearchQuery; + field?: string; + comparatorType?: string; + value?: string; + fieldType?: string; + minMatch?: number; + maxMatch?: number; +} + +interface Criteria { + criteriaId: string; + name: string; + createdAt: number; + updatedAt: number; + searchQuery: SearchQuery; +} + +class CriteriaCompletionChecker { + private localStoredEventList: any[]; + + constructor(localStoredEventList: string) { + this.localStoredEventList = JSON.parse(localStoredEventList); + } + + public getMatchedCriteria(criteriaData: string): string | null { + let criteriaId: string | null = null; + + try { + const json = JSON.parse(criteriaData); + if (json.criteriaSets) { + criteriaId = this.findMatchedCriteria(json.criteriaSets); + } + } catch (e) { + this.handleJSONException(e); + } + + return criteriaId; + } + + private findMatchedCriteria(criteriaList: Criteria[]): string | null { + 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); + } + return false; + }); + + // Return the criteriaId of the matching criteria or null if none found + return matchingCriteria ? matchingCriteria.criteriaId : null; + } + + private prepareEventsToProcess(): any[] { + const eventsToProcess: any[] = this.getEventsWithCartItems(); + const nonPurchaseEvents: any[] = this.getNonCartEvents(); + + nonPurchaseEvents.forEach((event) => { + eventsToProcess.push(event); + }); + + return eventsToProcess; + } + + private getEventsWithCartItems(): any[] { + const processedEvents: any[] = []; + + this.localStoredEventList.forEach((localEventData) => { + if ( + localEventData[SHARED_PREFS_EVENT_TYPE] && + localEventData[SHARED_PREFS_EVENT_TYPE] === TRACK_PURCHASE + ) { + const updatedItem: Record = {}; + + if (localEventData[KEY_ITEMS]) { + let items = localEventData[KEY_ITEMS]; + items = items.map((item: any) => { + const updatItem: any = {}; + Object.keys(item).forEach((key) => { + updatItem[`${PURCHASE_ITEM_PREFIX}${key}`] = item[key]; + }); + return updatItem; + }); + updatedItem[PURCHASE_ITEM] = items; + } + + if (localEventData.dataFields) { + Object.keys(localEventData.dataFields).forEach((key) => { + updatedItem[key] = localEventData.dataFields[key]; + }); + } + + Object.keys(localEventData).forEach((key) => { + if (key !== KEY_ITEMS && key !== 'dataFields') { + updatedItem[key] = localEventData[key]; + } + }); + processedEvents.push({ + ...updatedItem, + [SHARED_PREFS_EVENT_TYPE]: TRACK_PURCHASE + }); + } else if ( + localEventData[SHARED_PREFS_EVENT_TYPE] && + localEventData[SHARED_PREFS_EVENT_TYPE] === TRACK_UPDATE_CART + ) { + const updatedItem: any = {}; + + if (localEventData[KEY_ITEMS]) { + let items = localEventData[KEY_ITEMS]; + items = items.map((item: any) => { + const updatItem: any = {}; + Object.keys(item).forEach((key) => { + updatItem[`${UPDATECART_ITEM_PREFIX}${key}`] = item[key]; + }); + return updatItem; + }); + updatedItem[KEY_ITEMS] = items; + } + + if (localEventData.dataFields) { + Object.keys(localEventData.dataFields).forEach((key) => { + updatedItem[key] = localEventData.dataFields[key]; + }); + // eslint-disable-next-line no-param-reassign + delete localEventData.dataFields; + } + Object.keys(localEventData).forEach((key) => { + if (key !== KEY_ITEMS && key !== 'dataFields') { + if (key === SHARED_PREFS_EVENT_TYPE) { + updatedItem[key] = TRACK_EVENT; + } else { + updatedItem[key] = localEventData[key]; + } + } + }); + processedEvents.push({ + ...updatedItem, + [KEY_EVENT_NAME]: UPDATE_CART, + [SHARED_PREFS_EVENT_TYPE]: TRACK_EVENT + }); + } + }); + return processedEvents; + } + + private getNonCartEvents(): any[] { + const nonPurchaseEvents: any[] = []; + this.localStoredEventList.forEach((localEventData) => { + if ( + localEventData[SHARED_PREFS_EVENT_TYPE] && + (localEventData[SHARED_PREFS_EVENT_TYPE] === UPDATE_USER || + localEventData[SHARED_PREFS_EVENT_TYPE] === TRACK_EVENT) + ) { + const updatedItem: any = localEventData; + if (localEventData.dataFields) { + Object.keys(localEventData.dataFields).forEach((key) => { + updatedItem[key] = localEventData.dataFields[key]; + }); + // eslint-disable-next-line no-param-reassign + delete localEventData.dataFields; + } + nonPurchaseEvents.push(updatedItem); + } + }); + return nonPurchaseEvents; + } + + private evaluateTree( + node: SearchQuery | Criteria, + localEventData: any[] + ): boolean { + try { + if ((node as SearchQuery).searchQueries) { + const { combinator } = node as SearchQuery; + const { searchQueries } = node as SearchQuery; + if (combinator === 'And') { + /* eslint-disable-next-line @typescript-eslint/prefer-for-of */ + for (let i = 0; i < searchQueries.length; i += 1) { + if (!this.evaluateTree(searchQueries[i], localEventData)) { + return false; + } + } + return true; + } + if (combinator === 'Or') { + /* eslint-disable-next-line @typescript-eslint/prefer-for-of */ + for (let i = 0; i < searchQueries.length; i += 1) { + if (this.evaluateTree(searchQueries[i], localEventData)) { + return true; + } + } + return false; + } + if (combinator === 'Not') { + /* eslint-disable-next-line @typescript-eslint/prefer-for-of */ + for (let i = 0; i < searchQueries.length; i += 1) { + (searchQueries[i] as any).isNot = true; + if (this.evaluateTree(searchQueries[i], localEventData)) { + return false; + } + } + return true; + } + } else if ((node as SearchQuery).searchCombo) { + return this.evaluateSearchQueries(node as SearchQuery, localEventData); + } + } catch (e) { + this.handleException(e); + } + return false; + } + + /* eslint-disable no-continue */ + private evaluateSearchQueries( + node: SearchQuery, + localEventData: any[] + ): boolean { + // this function will compare the actualy searhqueues under search combo + for (let i = 0; i < localEventData.length; i += 1) { + const eventData = localEventData[i]; + const trackingType = eventData[SHARED_PREFS_EVENT_TYPE]; + const { dataType } = node; + if (dataType === trackingType) { + const { searchCombo } = node; + 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; + // eslint-disable-next-line no-param-reassign + node.minMatch = minMatch; + if (minMatch > 0) { + continue; + } + } + if (isNot && !(i + 1 === localEventData.length)) { + continue; + } + return true; + } + if (isNot) { + return false; + } + } + } + return false; + } + + private evaluateEvent( + localEvent: any, + searchQueries: any, + combinator: string + ): boolean { + if (combinator === 'And' || combinator === 'Or') { + return this.evaluateFieldLogic(searchQueries, localEvent); + } + if (combinator === 'Not') { + return !this.evaluateFieldLogic(searchQueries, localEvent); + } + 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; + let keyItem = null; + if (localDataKeys.includes(KEY_ITEMS)) { + keyItem = KEY_ITEMS; + } else if (localDataKeys.includes(PURCHASE_ITEM)) { + keyItem = PURCHASE_ITEM; + } + + if (keyItem !== null) { + // scenario of items inside purchase and updateCart Events + const items = eventData[keyItem]; + const result = items.some((item: any) => + this.doesItemMatchQueries(item, searchQueries) + ); + if (!result && this.doesItemCriteriaExists(searchQueries)) { + // items criteria existed and it did not match + return result; + } + itemMatchedResult = result; + } + const filteredLocalDataKeys = localDataKeys.filter( + (item: any) => item !== KEY_ITEMS + ); + + if (filteredLocalDataKeys.length === 0) { + return itemMatchedResult; + } + + const filteredSearchQueries = searchQueries.filter( + (searchQuery) => + !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; + if ( + query.dataType === TRACK_EVENT && + query.fieldType === 'object' && + query.comparatorType === 'IsSet' + ) { + const eventName = eventData[KEY_EVENT_NAME]; + if (eventName === UPDATE_CART && field === eventName) { + return true; + } + if (field === eventName) { + return true; + } + } + + const eventKeyItems = filteredLocalDataKeys.filter( + (keyItem) => keyItem === field + ); + + if (field.includes('.')) { + const splitField = field.split('.') as string[]; + const fields = + eventData?.eventType === TRACK_EVENT && + eventData?.eventName === splitField[0] + ? splitField.slice(1) + : splitField; + + let fieldValue = eventData; + let isSubFieldArray = false; + let isSubMatch = false; + + fields.forEach((subField) => { + const subFieldValue = fieldValue[subField]; + if (Array.isArray(subFieldValue)) { + isSubFieldArray = true; + isSubMatch = subFieldValue.some((item: any) => { + const data = fields.reduceRight((acc: any, key) => { + if (key === subField) { + return { [key]: item }; + } + return { [key]: acc }; + }, {}); + + return this.evaluateFieldLogic(searchQueries, { + ...eventData, + ...data + }); + }); + } else { + fieldValue = subFieldValue; + } + }); + + if (isSubFieldArray) { + return isSubMatch; + } + + const valueFromObj = this.getFieldValue(eventData, field); + if (valueFromObj) { + return this.evaluateComparison( + query.comparatorType, + valueFromObj, + query.value ?? query.values ?? '' + ); + } + } else if (eventKeyItems.length) { + return this.evaluateComparison( + query.comparatorType, + eventData[field], + query.value ?? query.values ?? '' + ); + } + return false; + }); + return matchResult; + } + + private getFieldValue(data: any, field: string): any { + const fields: string[] = field.split('.'); + if (data?.eventType === TRACK_EVENT && data?.eventName === fields[0]) { + fields.shift(); + } + return fields.reduce( + (value, currentField) => + value && value[currentField] !== undefined + ? value[currentField] + : undefined, + data + ); + } + + private doesItemMatchQueries(item: any, searchQueries: any[]): boolean { + let shouldReturn = false; + const filteredSearchQueries = searchQueries.filter((searchQuery) => { + if ( + searchQuery.field.startsWith(UPDATECART_ITEM_PREFIX) || + searchQuery.field.startsWith(PURCHASE_ITEM_PREFIX) + ) { + if (!Object.keys(item).includes(searchQuery.field)) { + shouldReturn = true; + return false; + } + return true; + } + return false; + }); + if (filteredSearchQueries.length === 0 || shouldReturn) { + return false; + } + return filteredSearchQueries.every((query: any) => { + const { field } = query; + if (Object.prototype.hasOwnProperty.call(item, field)) { + return this.evaluateComparison( + query.comparatorType, + item[field], + query.value ?? query.values ?? '' + ); + } + return false; + }); + } + + private evaluateComparison( + comparatorType: string, + matchObj: any, + valueToCompare: string | string[] + ): boolean { + if (!valueToCompare && comparatorType !== 'IsSet') { + return false; + } + switch (comparatorType) { + case 'Equals': + return this.compareValueEquality(matchObj, valueToCompare); + case 'DoesNotEqual': + return !this.compareValueEquality(matchObj, valueToCompare); + case 'IsSet': + return this.issetCheck(matchObj); + case 'GreaterThan': + case 'LessThan': + case 'GreaterThanOrEqualTo': + case 'LessThanOrEqualTo': + return this.compareNumericValues( + matchObj, + valueToCompare as string, + comparatorType + ); + case 'Contains': + return this.compareStringContains(matchObj, valueToCompare as string); + case 'StartsWith': + return this.compareStringStartsWith(matchObj, valueToCompare as string); + case 'MatchesRegex': + return this.compareWithRegex(matchObj, valueToCompare as string); + default: + return false; + } + } + + private compareValueEquality( + sourceTo: any, + stringValue: string | string[] + ): boolean { + if (Array.isArray(sourceTo)) { + return sourceTo.some((source) => + this.compareValueEquality(source, stringValue) + ); + } + + if (Array.isArray(stringValue)) { + return stringValue.some((value) => + this.compareValueEquality(sourceTo, value) + ); + } + + if ( + (typeof sourceTo === 'number' || typeof sourceTo === 'boolean') && + stringValue !== '' + ) { + // eslint-disable-next-line no-restricted-globals + if (typeof sourceTo === 'number' && !isNaN(parseFloat(stringValue))) { + return sourceTo === parseFloat(stringValue); + } + if (typeof sourceTo === 'boolean') { + return sourceTo === (stringValue === 'true'); + } + } else if (typeof sourceTo === 'string') { + return sourceTo === stringValue; + } + return false; + } + + private compareNumericValues( + sourceTo: any, + stringValue: string, + compareOperator: string + ): boolean { + // eslint-disable-next-line no-restricted-globals + if (Array.isArray(sourceTo)) { + return sourceTo.some((source) => + this.compareNumericValues(source, stringValue, compareOperator) + ); + } + + if (!Number.isNaN(parseFloat(stringValue))) { + const sourceNumber = parseFloat(sourceTo); + const numericValue = parseFloat(stringValue); + switch (compareOperator) { + case 'GreaterThan': + return sourceNumber > numericValue; + case 'LessThan': + return sourceNumber < numericValue; + case 'GreaterThanOrEqualTo': + return sourceNumber >= numericValue; + case 'LessThanOrEqualTo': + return sourceNumber <= numericValue; + default: + return false; + } + } + return false; + } + + private compareStringContains(sourceTo: any, stringValue: string): boolean { + if (Array.isArray(sourceTo)) { + return sourceTo.some((source) => + this.compareStringContains(source, stringValue) + ); + } + return ( + (typeof sourceTo === 'string' || typeof sourceTo === 'object') && + sourceTo.includes(stringValue) + ); + } + + private compareStringStartsWith(sourceTo: any, stringValue: string): boolean { + if (Array.isArray(sourceTo)) { + return sourceTo.some((source) => + this.compareStringStartsWith(source, stringValue) + ); + } + return typeof sourceTo === 'string' && sourceTo.startsWith(stringValue); + } + + private compareWithRegex(sourceTo: string, pattern: string): boolean { + if (Array.isArray(sourceTo)) { + return sourceTo.some((source) => this.compareWithRegex(source, pattern)); + } + try { + const regexPattern = new RegExp(pattern); + return regexPattern.test(sourceTo); + } catch (e) { + console.error(e); + return false; + } + } + + private issetCheck(matchObj: string | object | any[]): boolean { + if (Array.isArray(matchObj)) { + return matchObj.length > 0; + } + if (typeof matchObj === 'object' && matchObj !== null) { + return Object.keys(matchObj).length > 0; + } + return matchObj !== ''; + } + + private handleException(e: any) { + console.error('Exception occurred', e.toString()); + } + + private handleJSONException(e: any) { + console.error('JSONException occurred', e.toString()); + } +} + +export default CriteriaCompletionChecker; diff --git a/src/anonymousUserTracking/tests/anonymousUserEventManager.test.ts b/src/anonymousUserTracking/tests/anonymousUserEventManager.test.ts new file mode 100644 index 00000000..64826966 --- /dev/null +++ b/src/anonymousUserTracking/tests/anonymousUserEventManager.test.ts @@ -0,0 +1,519 @@ +import { AnonymousUserEventManager } from '../anonymousUserEventManager'; +import { baseIterableRequest } from '../../request'; +import { + SHARED_PREFS_ANON_SESSIONS, + SHARED_PREFS_EVENT_LIST_KEY, + SHARED_PREFS_CRITERIA, + SHARED_PREF_ANON_USAGE_TRACKED +} from '../../constants'; +import { UpdateUserParams } from '../../users'; +import { TrackPurchaseRequestParams } from '../../commerce'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +jest.mock('../criteriaCompletionChecker', () => + jest.fn().mockImplementation(() => ({ + getMatchedCriteria: jest.fn() + })) +); + +jest.mock('../../request', () => ({ + baseIterableRequest: jest.fn() +})); + +declare global { + function uuidv4(): string; + function getEmail(): string; + function getUserID(): string; + function setUserID(): string; +} + +describe('AnonymousUserEventManager', () => { + let anonUserEventManager: AnonymousUserEventManager; + + beforeEach(() => { + (global as any).localStorage = localStorageMock; + anonUserEventManager = new AnonymousUserEventManager(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should update anonymous session information correctly', () => { + const initialAnonSessionInfo = { + itbl_anon_sessions: { + number_of_sessions: 1, + first_session: 123456789, + last_session: expect.any(Number) + } + }; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + + anonUserEventManager.updateAnonSession(); + + expect(localStorageMock.setItem).toHaveBeenCalledWith( + SHARED_PREFS_ANON_SESSIONS, + expect.stringContaining('itbl_anon_sessions') + ); + }); + + it('should set criteria data in localStorage when baseIterableRequest succeeds', async () => { + const mockResponse = { data: { criteria: 'mockCriteria' } }; + (baseIterableRequest as jest.Mock).mockResolvedValueOnce(mockResponse); + + const setItemMock = jest.spyOn(localStorage, 'setItem'); + await anonUserEventManager.getAnonCriteria(); + + expect(setItemMock).toHaveBeenCalledWith( + SHARED_PREFS_CRITERIA, + '{"criteria":"mockCriteria"}' + ); + }); + + it('should create known user and make API request when userData is available', async () => { + const userData = { + number_of_sessions: 5, + last_session: 123456789, + first_session: 123456789 + }; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === 'criteria') { + return JSON.stringify({ + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'testEvent', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(userData); + } + return null; + }); + + anonUserEventManager.updateAnonSession(); + }); + + it('should call createKnownUser when trackAnonEvent is called', async () => { + const payload = { + eventName: 'testEvent', + eventType: 'customEvent' + }; + const eventData = [ + { + eventName: 'testEvent', + createdAt: 1708494757530, + dataFields: undefined, + createNewFields: true, + eventType: 'customEvent' + } + ]; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === 'criteria') { + return JSON.stringify({ + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'testEvent', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }); + } + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify(eventData); + } + return null; + }); + await anonUserEventManager.trackAnonEvent(payload); + }); + + it('should not call createKnownUser when trackAnonEvent is called and criteria does not match', async () => { + const payload = { + eventName: 'Event' + }; + const eventData = [ + { + eventName: 'Event', + createdAt: 1708494757530, + dataFields: undefined, + createNewFields: true, + eventType: 'customEvent' + } + ]; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === 'criteria') { + return JSON.stringify({ + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'testEvent', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }); + } + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify(eventData); + } + return null; + }); + await anonUserEventManager.trackAnonEvent(payload); + }); + + it('should not call createKnownUser when trackAnonEvent is called and criteria not find', async () => { + const payload = { + eventName: 'Event' + }; + const eventData = [ + { + eventName: 'Event', + createdAt: 1708494757530, + dataFields: undefined, + createNewFields: true, + eventType: 'customEvent' + } + ]; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === 'criteria') { + return null; + } + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify(eventData); + } + return null; + }); + await anonUserEventManager.trackAnonEvent(payload); + }); + + it('should call createKnownUser when trackAnonUpdateUser is called', async () => { + const payload: UpdateUserParams = { + dataFields: { country: 'UK' } + }; + const userData = [ + { + userId: 'user', + createdAt: 1708494757530, + dataFields: undefined, + createNewFields: true, + eventType: 'updateUser' + } + ]; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === 'criteria') { + return JSON.stringify({ + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'UpdateUserCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'updateUser', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'updateUser', + field: 'userId', + comparatorType: 'Equals', + value: 'user', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }); + } + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify(userData); + } + return null; + }); + await anonUserEventManager.trackAnonUpdateUser(payload); + }); + + it('should call createKnownUser when trackAnonPurchaseEvent is called', async () => { + const payload: TrackPurchaseRequestParams = { + items: [ + { + id: '123', + name: 'Black Coffee', + quantity: 1, + price: 4 + } + ], + total: 0 + }; + const userData = [ + { + items: [ + { + id: '123', + name: 'Black Coffee', + quantity: 1, + price: 4 + } + ], + user: { + userId: 'user' + }, + total: 0, + eventType: 'purchase' + } + ]; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === 'criteria') { + return JSON.stringify({ + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'shoppingCartItemsCriteria', + 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: 'Black Coffee', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '4.00', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }); + } + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify(userData); + } + return null; + }); + await anonUserEventManager.trackAnonPurchaseEvent(payload); + }); + + it('should call createKnownUser when trackAnonUpdateCart is called', async () => { + const payload: TrackPurchaseRequestParams = { + items: [ + { + id: '123', + name: 'Black Coffee', + quantity: 2, + price: 4 + } + ], + total: 0 + }; + const userData = [ + { + items: [ + { + id: '123', + name: 'Black Coffee', + quantity: 1, + price: 4 + } + ], + user: { + userId: 'user' + }, + total: 0, + eventType: 'cartUpdate' + } + ]; + + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === 'criteria') { + return JSON.stringify({ + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'CartUpdateItemsCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'cartUpdate', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'cartUpdate', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'Black Coffee', + fieldType: 'string' + }, + { + dataType: 'cartUpdate', + field: 'shoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '4.00', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }); + } + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify(userData); + } + return null; + }); + await anonUserEventManager.trackAnonUpdateCart(payload); + }); +}); diff --git a/src/anonymousUserTracking/tests/combinationLogicCriteria.test.ts b/src/anonymousUserTracking/tests/combinationLogicCriteria.test.ts new file mode 100644 index 00000000..17d339bb --- /dev/null +++ b/src/anonymousUserTracking/tests/combinationLogicCriteria.test.ts @@ -0,0 +1,1985 @@ +import { SHARED_PREFS_EVENT_LIST_KEY } from '../../constants'; +import CriteriaCompletionChecker from '../criteriaCompletionChecker'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +describe('CombinationLogicCriteria', () => { + beforeEach(() => { + (global as any).localStorage = localStorageMock; + }); + + it('should return criteriaId 1 if Contact Property AND Custom Event is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { total: 10 }, + eventType: 'customEvent' + }, + { + dataFields: { firstName: 'David' }, + 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, + criteriaSets: [ + { + criteriaId: '1', + name: 'Combination Logic - Contact Property AND Custom Event', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'total', + comparatorType: 'Equals', + value: '10', + id: 6, + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('1'); + }); + + it('should return null (combination logic criteria 1 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { total: 10 }, + eventType: 'customEvent' + }, + { + dataFields: { firstName: 'Davidson' }, + 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, + criteriaSets: [ + { + criteriaId: '1', + name: 'Combination Logic - Contact Property AND Custom Event', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'total', + comparatorType: 'Equals', + value: '10', + id: 6, + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 2 if Contact Property OR Custom Event is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { total: 10 }, + eventType: 'customEvent' + }, + { + dataFields: { firstName: 'David' }, + 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, + criteriaSets: [ + { + criteriaId: '2', + name: 'Combination Logic - Contact Property OR Custom Event', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'total', + comparatorType: 'Equals', + value: '10', + id: 6, + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('2'); + }); + + it('should return null (combination logic criteria 2 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { total: 101 }, + eventType: 'customEvent' + }, + { + dataFields: { firstName: 'Davidson' }, + 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, + criteriaSets: [ + { + criteriaId: '2', + name: 'Combination Logic - Contact Property OR Custom Event', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'total', + comparatorType: 'Equals', + value: '10', + id: 6, + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 3 if Contact Property NOR (NOT) Custom Event is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + itesm: [{ name: 'Cofee', id: 'fdsafds', price: 10, quantity: 2 }], + total: 10 + }, + eventType: 'customEvent' + }, + { + dataFields: { firstName: 'Davidson' }, + 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, + criteriaSets: [ + { + criteriaId: '3', + name: 'Combination Logic - Contact Property NOR (NOT) Custom Event', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'total', + comparatorType: 'Equals', + value: '10', + id: 6, + fieldType: 'double' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('3'); + }); + + it('should return null (combination logic criteria 3 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { total: 1 }, + eventType: 'customEvent' + }, + { + dataFields: { firstName: 'David' }, + 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, + criteriaSets: [ + { + criteriaId: '3', + name: 'Combination Logic - Contact Property NOR (NOT) Custom Event', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'total', + comparatorType: 'Equals', + value: '10', + id: 6, + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 4 if UpdateCart AND Contact Property is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'fried', id: 'fried', price: 10, quantity: 2 }], + eventType: 'cartUpdate' + }, + { + dataFields: { firstName: 'David' }, + 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, + criteriaSets: [ + { + criteriaId: '4', + name: 'Combination Logic - UpdateCart AND Contact Property', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('4'); + }); + + it('should return null (combination logic criteria 4 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 2 }], + eventType: 'cartUpdate' + }, + { + dataFields: { firstName: 'Davidson' }, + 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, + criteriaSets: [ + { + criteriaId: '4', + name: 'Combination Logic - UpdateCart AND Contact Property', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 5 if UpdateCart OR Contact Property is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'fried', id: 'fried', price: 10, quantity: 2 }], + eventType: 'cartUpdate' + }, + { + dataFields: { firstName: 'David' }, + 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, + criteriaSets: [ + { + criteriaId: '5', + name: 'Combination Logic - UpdateCart OR Contact Property', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('5'); + }); + + it('should return null (combination logic criteria 5 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 2 }], + eventType: 'cartUpdate' + }, + { + dataFields: { firstName: 'Davidson' }, + 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, + criteriaSets: [ + { + criteriaId: '5', + name: 'Combination Logic - UpdateCart OR Contact Property', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 6 if UpdateCart NOR (NOT) Contact Property is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'boiled', id: 'boiled', price: 10, quantity: 2 }], + eventType: 'cartUpdate' + }, + { + dataFields: { firstName: 'Davidson' }, + 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, + criteriaSets: [ + { + criteriaId: '6', + name: 'Combination Logic - UpdateCart NOR (NOT) Contact Property', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + }); + + it('should return null (combination logic criteria 6 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'fried', id: 'fried', price: 10, quantity: 2 }], + eventType: 'cartUpdate' + }, + { + dataFields: { firstName: 'David' }, + 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, + criteriaSets: [ + { + criteriaId: '6', + name: 'Combination Logic - UpdateCart NOR (NOT) Contact Property', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'Equals', + value: 'David', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 7 if Purchase AND UpdateCart is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 2 }], + eventType: 'purchase' + }, + { + items: [{ name: 'fried', id: 'fried', 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, + criteriaSets: [ + { + criteriaId: '7', + name: 'Combination Logic - Purchase AND UpdateCart', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('7'); + }); + + it('should return null (combination logic criteria 7 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'fried', id: 'fried', price: 10, quantity: 2 }], + eventType: 'purchase' + }, + { + items: [{ name: 'chicken', id: 'chicken', 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, + criteriaSets: [ + { + criteriaId: '7', + name: 'Combination Logic - Purchase AND UpdateCart', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 8 if Purchase OR UpdateCart is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 2 }], + eventType: 'purchase' + }, + { + items: [{ name: 'fried', id: 'fried', 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, + criteriaSets: [ + { + criteriaId: '8', + name: 'Combination Logic - Purchase OR UpdateCart', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('8'); + }); + + it('should return null (combination logic criteria 8 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'fried', id: 'fried', price: 10, quantity: 2 }], + eventType: 'purchase' + }, + { + items: [{ name: 'chicken', id: 'chicken', 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, + criteriaSets: [ + { + criteriaId: '8', + name: 'Combination Logic - Purchase OR UpdateCart', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 9 if Purchase NOR (NOT) UpdateCart is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'beef', id: 'beef', price: 10, quantity: 2 }], + eventType: 'purchase' + }, + { + items: [{ name: 'boiled', id: 'boiled', 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, + criteriaSets: [ + { + criteriaId: '9', + name: 'Combination Logic - Purchase NOR (NOT) UpdateCart', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('9'); + }); + + it('should return null (combination logic criteria 9 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 2 }], + eventType: 'purchase' + }, + { + items: [{ name: 'fried', id: 'fried', 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, + criteriaSets: [ + { + criteriaId: '9', + name: 'Combination Logic - Purchase NOR (NOT) UpdateCart', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'Equals', + value: 'fried', + id: 2, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 10 if Custom Event AND Purchase is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { eventName: 'birthday' }, + eventType: 'customEvent' + }, + { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 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, + criteriaSets: [ + { + criteriaId: '10', + name: 'Combination Logic - Custom Event AND Purchase', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'birthday', + id: 16, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('10'); + }); + + it('should return null (combination logic criteria 10 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { eventName: 'anniversary' }, + eventType: 'customEvent' + }, + { + items: [{ name: 'fried', id: 'fried', price: 10, quantity: 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, + criteriaSets: [ + { + criteriaId: '10', + name: 'Combination Logic - Custom Event AND Purchase', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'birthday', + id: 16, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 11 if Custom Event OR Purchase is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { eventName: 'birthday' }, + eventType: 'customEvent' + } + /* { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 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, + criteriaSets: [ + { + criteriaId: '11', + name: 'Combination Logic - Custom Event OR Purchase', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'birthday', + id: 16, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('11'); + }); + + it('should return null (combination logic criteria 11 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { eventName: 'anniversary' }, + eventType: 'customEvent' + }, + { + items: [{ name: 'fried', id: 'fried', price: 10, quantity: 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, + criteriaSets: [ + { + criteriaId: '11', + name: 'Combination Logic - Custom Event OR Purchase', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'birthday', + id: 16, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 12 if Custom Event NOR (NOT) Purchase is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { eventName: 'anniversary' }, + eventType: 'customEvent' + }, + { + items: [{ name: 'beef', id: 'beef', price: 10, quantity: 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, + criteriaSets: [ + { + criteriaId: '12', + name: 'Combination Logic - Custom Event NOR (NOT) Purchase', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'birthday', + id: 16, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('12'); + }); + + it('should return null (combination logic criteria 12 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { eventName: 'birthday' }, + eventType: 'customEvent' + }, + { + items: [{ name: 'chicken', id: 'chicken', price: 10, quantity: 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, + criteriaSets: [ + { + criteriaId: '12', + name: 'Combination Logic - Custom Event NOR (NOT) Purchase', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'chicken', + id: 13, + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'birthday', + id: 16, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); +}); diff --git a/src/anonymousUserTracking/tests/compareArrayDataTypes.test.ts b/src/anonymousUserTracking/tests/compareArrayDataTypes.test.ts new file mode 100644 index 00000000..4440ca14 --- /dev/null +++ b/src/anonymousUserTracking/tests/compareArrayDataTypes.test.ts @@ -0,0 +1,739 @@ +import { SHARED_PREFS_EVENT_LIST_KEY } from '../../constants'; +import CriteriaCompletionChecker from '../criteriaCompletionChecker'; +import { + IS_NOT_ONE_OF_CRITERIA, + ARRAY_CONTAINS_CRITERIA, + ARRAY_DOES_NOT_EQUAL_CRITERIA, + ARRAY_EQUAL_CRITERIA, + ARRAY_GREATER_THAN_CRITERIA, + ARRAY_GREATER_THAN_EQUAL_TO_CRITERIA, + ARRAY_LESS_THAN_CRITERIA, + ARRAY_LESS_THAN_EQUAL_TO_CRITERIA, + ARRAY_MATCHREGEX_CRITERIA, + ARRAY_STARTSWITH_CRITERIA, + IS_ONE_OF_CRITERIA +} from './constants'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +describe('compareArrayDataTypes', () => { + beforeEach(() => { + (global as any).localStorage = localStorageMock; + }); + + // MARK: Equal + it('should return criteriaId 285 (compare array Equal)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1996, 1997, 2002, 2020, 2024], + score: [10.5, 11.5, 12.5, 13.5, 14.5], + timestamp: [ + 1722497422151, 1722500235276, 1722500215276, 1722500225276, + 1722500245276 + ] + }, + eventType: 'user' + }, + { + eventName: 'button-clicked', + dataFields: { + animal: ['cat', 'dog', 'giraffe'] + }, + 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(ARRAY_EQUAL_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array Equal - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1996, 1998, 2002, 2020, 2024], + score: [12.5, 13.5, 14.5], + timestamp: [ + 1722497422151, 1722500235276, 1722500225276, 1722500245276 + ] + }, + eventType: 'user' + }, + { + eventName: 'button-clicked', + dataFields: { + animal: ['cat', 'dog'] + }, + 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(ARRAY_EQUAL_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: DoesNotEqual + it('should return criteriaId 285 (compare array DoesNotEqual)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1996, 1998, 2002, 2020, 2024], + score: [12.5, 13.5, 14.5], + timestamp: [ + 1722497422151, 1722500235276, 1722500225276, 1722500245276 + ] + }, + eventType: 'user' + }, + { + eventName: 'button-clicked', + dataFields: { + animal: ['cat', 'dog'] + }, + 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(ARRAY_DOES_NOT_EQUAL_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array DoesNotEqual - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1996, 1997, 2002, 2020, 2024], + score: [10.5, 11.5, 12.5, 13.5, 14.5], + timestamp: [ + 1722497422151, 1722500235276, 1722500215276, 1722500225276, + 1722500245276 + ] + }, + eventType: 'user' + }, + { + eventName: 'button-clicked', + dataFields: { + animal: ['cat', 'dog', 'giraffe'] + }, + 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(ARRAY_DOES_NOT_EQUAL_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: GreaterThan + it('should return criteriaId 285 (compare array GreaterThan)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1996, 1998, 2002, 2020, 2024] + }, + 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(ARRAY_GREATER_THAN_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array GreaterThan - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1990, 1992, 1994, 1997] + }, + 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(ARRAY_GREATER_THAN_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: LessThan + it('should return criteriaId 285 (compare array LessThan)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1990, 1992, 1994, 1996, 1998] + }, + 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(ARRAY_LESS_THAN_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array LessThan - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1997, 1999, 2002, 2004] + }, + 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(ARRAY_LESS_THAN_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: GreaterThanOrEqualTo + it('should return criteriaId 285 (compare array GreaterThanOrEqualTo)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1997, 1998, 2002, 2020, 2024] + }, + 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(ARRAY_GREATER_THAN_EQUAL_TO_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array GreaterThanOrEqualTo - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1990, 1992, 1994, 1996] + }, + 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(ARRAY_GREATER_THAN_EQUAL_TO_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: LessThanOrEqualTo + it('should return criteriaId 285 (compare array LessThanOrEqualTo)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1990, 1992, 1994, 1996, 1998] + }, + 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(ARRAY_LESS_THAN_EQUAL_TO_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array LessThanOrEqualTo - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + milestoneYears: [1998, 1999, 2002, 2004] + }, + 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(ARRAY_LESS_THAN_EQUAL_TO_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: Contains + it('should return criteriaId 285 (compare array Contains)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + addresses: [ + 'New York, US', + 'San Francisco, US', + 'San Diego, US', + 'Los Angeles, US', + 'Tokyo, JP', + 'Berlin, DE', + 'London, GB' + ] + }, + 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(ARRAY_CONTAINS_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array Contains - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + addresses: ['Tokyo, JP', 'Berlin, DE', 'London, GB'] + }, + 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(ARRAY_CONTAINS_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: StartsWith + it('should return criteriaId 285 (compare array StartsWith)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + addresses: [ + 'US, New York', + 'US, San Francisco', + 'US, San Diego', + 'US, Los Angeles', + 'JP, Tokyo', + 'DE, Berlin', + 'GB, London' + ] + }, + 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(ARRAY_STARTSWITH_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array StartsWith - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + addresses: ['JP, Tokyo', 'DE, Berlin', 'GB, London'] + }, + 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(ARRAY_STARTSWITH_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: MatchesRegex + it('should return criteriaId 285 (compare array MatchesRegex)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + addresses: [ + 'US, New York', + 'US, San Francisco', + 'US, San Diego', + 'US, Los Angeles', + 'JP, Tokyo', + 'DE, Berlin', + 'GB, London' + ] + }, + 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(ARRAY_MATCHREGEX_CRITERIA) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId null (compare array MatchesRegex - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + addresses: [ + 'US, New York', + 'US, San Francisco', + 'US, San Diego', + 'US, Los Angeles' + ] + }, + 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(ARRAY_MATCHREGEX_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: IsOneOf + it('should return criteriaId 299 (compare array IsOneOf)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + country: 'China', + addresses: ['US', 'UK', 'JP', 'DE', 'GB'] + }, + 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(IS_ONE_OF_CRITERIA) + ); + expect(result).toEqual('299'); + }); + + it('should return criteriaId null (compare array IsOneOf - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + country: 'Korea', + addresses: ['US', 'UK'] + }, + 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(IS_ONE_OF_CRITERIA) + ); + expect(result).toEqual(null); + }); + + // MARK: IsNotOneOf + it('should return criteriaId 299 (compare array IsNotOneOf)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + country: 'Korea', + addresses: ['US', 'UK'] + }, + 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(IS_NOT_ONE_OF_CRITERIA) + ); + expect(result).toEqual('299'); + }); + + it('should return criteriaId null (compare array IsNotOneOf - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + country: 'China', + addresses: ['US', 'UK', 'JP', 'DE', 'GB'] + }, + 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(IS_NOT_ONE_OF_CRITERIA) + ); + expect(result).toEqual(null); + }); +}); diff --git a/src/anonymousUserTracking/tests/complexCriteria.test.ts b/src/anonymousUserTracking/tests/complexCriteria.test.ts new file mode 100644 index 00000000..973262eb --- /dev/null +++ b/src/anonymousUserTracking/tests/complexCriteria.test.ts @@ -0,0 +1,1854 @@ +import { SHARED_PREFS_EVENT_LIST_KEY } from '../../constants'; +import CriteriaCompletionChecker from '../criteriaCompletionChecker'; +import { + COMPLEX_CRITERIA_1, + COMPLEX_CRITERIA_2, + COMPLEX_CRITERIA_3 +} from './constants'; + +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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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: { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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); + }); + + it('should return criteriaId 134 (Min-Max 2)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ id: '12', name: 'Mocha', price: 50, quantity: 50 }], + eventType: 'cartUpdate' + }, + { + items: [{ id: '12', name: 'Mocha', price: 50.0, quantity: 50 }], + eventType: 'cartUpdate' + }, + { + 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, + criteriaSets: [ + { + criteriaId: '134', + name: 'Min-Max 2', + createdAt: 1719336370734, + updatedAt: 1719337067199, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'Equals', + value: '50.0', + fieldType: 'double' + } + ] + }, + minMatch: 2, + maxMatch: 3 + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'preferred_car_models', + comparatorType: 'Equals', + value: 'Honda', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('134'); + }); + + it('should return criteriaId 151 (sampleTest1)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + 'animal-found': { + type: 'cat', + count: 4 + } + }, + eventType: 'customEvent' + }, + { + items: [{ id: '12', name: 'Caramel', price: 3, 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, + criteriaSets: [ + { + criteriaId: '151', + name: 'test criteria', + createdAt: 1719336370734, + updatedAt: 1719337067199, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'animal-found.type', + comparatorType: 'Equals', + value: 'cat', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'animal-found.count', + comparatorType: 'LessThan', + value: '5', + fieldType: 'long' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '500', + fieldType: 'double' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'LessThan', + value: '20', + fieldType: 'double' + } + ] + } + } + ] + }, + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'Caramel', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.quantity', + comparatorType: 'GreaterThanOrEqualTo', + value: '2', + fieldType: 'long' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'country', + comparatorType: 'Equals', + value: 'UK', + fieldType: 'string' + }, + { + dataType: 'user', + field: 'likes_boba', + comparatorType: 'Equals', + value: 'false', + fieldType: 'boolean' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('151'); + }); + + it('should return null (sampleTest1 fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + 'animal-found.type': 'dog', + 'animal-found.count': 4 + }, + eventType: 'customEvent' + }, + { + items: [{ id: '12', name: 'Caramel', price: 3, 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, + criteriaSets: [ + { + criteriaId: '151', + name: 'test criteria', + createdAt: 1719336370734, + updatedAt: 1719337067199, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'animal-found.type', + comparatorType: 'Equals', + value: 'cat', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'animal-found.count', + comparatorType: 'LessThan', + value: '5', + fieldType: 'long' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '500', + fieldType: 'double' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'LessThan', + value: '20', + fieldType: 'double' + } + ] + } + } + ] + }, + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.name', + comparatorType: 'Equals', + value: 'Caramel', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.quantity', + comparatorType: 'GreaterThanOrEqualTo', + value: '2', + fieldType: 'long' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'country', + comparatorType: 'Equals', + value: 'UK', + fieldType: 'string' + }, + { + dataType: 'user', + field: 'likes_boba', + comparatorType: 'Equals', + value: 'false', + fieldType: 'boolean' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + // MARK: Complex criteria #1 + it('should return criteriaId 290 if (1 OR 2 OR 3) AND (4 AND 5) AND (6 NOT 7) matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + saved_cars: { color: 'black' }, + 'animal-found': { vaccinated: true }, + eventName: 'birthday' + }, + eventType: 'customEvent' + }, + { + dataFields: { reason: 'testing', total: 30 }, + eventType: 'purchase' + }, + { + dataFields: { firstName: 'Adam' }, + 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(COMPLEX_CRITERIA_1) + ); + expect(result).toEqual('290'); + }); + + it('should return criteriaId null if (1 OR 2 OR 3) AND (4 AND 5) AND (6 NOT 7) - No match', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventType: 'user', + dataFields: { + firstName: 'Alex' + } + }, + { + eventType: 'customEvent', + eventName: 'saved_cars', + dataFields: { + color: '' + } + }, + { + eventType: 'customEvent', + eventName: 'animal-found', + dataFields: { + vaccinated: true + } + }, + { + eventType: 'purchase', + dataFields: { + total: 30, + reason: 'testing' + } + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify(COMPLEX_CRITERIA_1) + ); + expect(result).toEqual(null); + }); + + // MARK: Complex criteria #2 + it('should return criteriaId 291 if (6 OR 7) OR (4 AND 5) OR (1 NOT 2 NOT 3) matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventType: 'user', + dataFields: { + firstName: 'xcode' + } + }, + { + eventType: 'customEvent', + eventName: 'saved_cars', + dataFields: { + color: 'black' + } + }, + { + eventType: 'customEvent', + eventName: 'animal-found', + dataFields: { + vaccinated: true + } + }, + { + eventType: 'purchase', + dataFields: { + total: 110, + reason: 'testing' + } + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify(COMPLEX_CRITERIA_2) + ); + expect(result).toEqual('291'); + }); + + it('should return criteriaId null if (6 OR 7) OR (4 AND 5) OR (1 NOT 2 NOT 3) - No match', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventType: 'user', + dataFields: { + firstName: 'Alex' + } + }, + { + eventType: 'purchase', + dataFields: { + total: 10, + reason: 'null' + } + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify(COMPLEX_CRITERIA_2) + ); + expect(result).toEqual(null); + }); + + // MARK: Complex criteria #3 + it('should return criteriaId 292 if (1 AND 2) NOR (3 OR 4 OR 5) NOR (6 NOR 7) matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataType: 'user', + dataFields: { + firstName: 'xcode', + lastName: 'ssr' + } + }, + { + dataType: 'customEvent', + eventName: 'animal-found', + dataFields: { + vaccinated: true, + count: 10 + } + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify(COMPLEX_CRITERIA_3) + ); + expect(result).toEqual('292'); + }); + + it('should return criteriaId null if (1 AND 2) NOR (3 OR 4 OR 5) NOR (6 NOR 7) - No match', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventType: 'user', + dataFields: { + firstName: 'Alex', + lastName: 'Aris' + } + }, + { + eventType: 'customEvent', + eventName: 'animal-found', + dataFields: { + vaccinated: false, + count: 4 + } + } + ]); + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify(COMPLEX_CRITERIA_3) + ); + expect(result).toEqual(null); + }); +}); diff --git a/src/anonymousUserTracking/tests/constants.ts b/src/anonymousUserTracking/tests/constants.ts new file mode 100644 index 00000000..b8f1c047 --- /dev/null +++ b/src/anonymousUserTracking/tests/constants.ts @@ -0,0 +1,1581 @@ +// CRITERIA TEST CONSTANTS + +export const DATA_TYPE_COMPARATOR_EQUALS = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'eventTimeStamp', + comparatorType: 'Equals', + value: '3', + fieldType: 'long' + }, + { + dataType: 'user', + field: 'savings', + comparatorType: 'Equals', + value: '19.99', + fieldType: 'double' + }, + { + dataType: 'user', + field: 'likes_boba', + comparatorType: 'Equals', + value: 'true', + fieldType: 'boolean' + }, + { + dataType: 'user', + field: 'country', + comparatorType: 'Equals', + value: 'Chaina', + fieldType: 'String' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const DATA_TYPE_COMPARATOR_DOES_NOT_EQUAL = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'eventTimeStamp', + comparatorType: 'DoesNotEqual', + value: '3', + fieldType: 'long' + }, + { + dataType: 'user', + field: 'savings', + comparatorType: 'DoesNotEqual', + value: '19.99', + fieldType: 'double' + }, + { + dataType: 'user', + field: 'likes_boba', + comparatorType: 'DoesNotEqual', + value: 'true', + fieldType: 'boolean' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const DATA_TYPE_COMPARATOR_LESS_THAN = { + count: 1, + criteriaSets: [ + { + criteriaId: '289', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'eventTimeStamp', + comparatorType: 'LessThan', + value: '15', + fieldType: 'long' + }, + { + dataType: 'user', + field: 'savings', + comparatorType: 'LessThan', + value: '15', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const DATA_TYPE_COMPARATOR_LESS_THAN_OR_EQUAL_TO = { + count: 1, + criteriaSets: [ + { + criteriaId: '290', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'eventTimeStamp', + comparatorType: 'LessThanOrEqualTo', + value: '17', + fieldType: 'long' + }, + { + dataType: 'user', + field: 'savings', + comparatorType: 'LessThanOrEqualTo', + value: '17', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const DATA_TYPE_COMPARATOR_GREATER_THAN = { + count: 1, + criteriaSets: [ + { + criteriaId: '290', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'eventTimeStamp', + comparatorType: 'GreaterThan', + value: '50', + fieldType: 'long' + }, + { + dataType: 'user', + field: 'savings', + comparatorType: 'GreaterThan', + value: '55', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const DATA_TYPE_COMPARATOR_GREATER_THAN_OR_EQUAL_TO = { + count: 1, + criteriaSets: [ + { + criteriaId: '291', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'eventTimeStamp', + comparatorType: 'GreaterThanOrEqualTo', + value: '20', + fieldType: 'long' + }, + { + dataType: 'user', + field: 'savings', + comparatorType: 'GreaterThanOrEqualTo', + value: '20', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const DATA_TYPE_COMPARATOR_IS_SET = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'eventTimeStamp', + comparatorType: 'IsSet', + value: '', + fieldType: 'long' + }, + { + dataType: 'user', + field: 'savings', + comparatorType: 'IsSet', + value: '', + fieldType: 'double' + }, + { + dataType: 'user', + field: 'saved_cars', + comparatorType: 'IsSet', + value: '', + fieldType: 'double' + }, + { + dataType: 'user', + field: 'country', + comparatorType: 'IsSet', + value: '', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_EQUAL_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_Array_Equal', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'milestoneYears', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'user', + id: 2, + value: '1997' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'score', + fieldType: 'double', + comparatorType: 'Equals', + dataType: 'user', + id: 2, + value: '11.5' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'timestamp', + fieldType: 'long', + comparatorType: 'Equals', + dataType: 'user', + id: 2, + valueLong: 1722500215276, + value: '1722500215276' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'button-clicked.animal', + fieldType: 'string', + comparatorType: 'Equals', + dataType: 'customEvent', + id: 25, + value: 'giraffe' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_DOES_NOT_EQUAL_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_Array_DoesNotEqual', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'milestoneYears', + fieldType: 'string', + comparatorType: 'DoesNotEqual', + dataType: 'user', + id: 2, + value: '1997' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'score', + fieldType: 'double', + comparatorType: 'DoesNotEqual', + dataType: 'user', + id: 2, + value: '11.5' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'timestamp', + fieldType: 'long', + comparatorType: 'DoesNotEqual', + dataType: 'user', + id: 2, + valueLong: 1722500215276, + value: '1722500215276' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'button-clicked.animal', + fieldType: 'string', + comparatorType: 'DoesNotEqual', + dataType: 'customEvent', + id: 25, + value: 'giraffe' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_GREATER_THAN_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'milestoneYears', + fieldType: 'string', + comparatorType: 'GreaterThan', + dataType: 'user', + id: 2, + value: '1997' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_LESS_THAN_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'milestoneYears', + fieldType: 'string', + comparatorType: 'LessThan', + dataType: 'user', + id: 2, + value: '1997' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_GREATER_THAN_EQUAL_TO_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'milestoneYears', + fieldType: 'string', + comparatorType: 'GreaterThanOrEqualTo', + dataType: 'user', + id: 2, + value: '1997' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_LESS_THAN_EQUAL_TO_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'milestoneYears', + fieldType: 'string', + comparatorType: 'LessThanOrEqualTo', + dataType: 'user', + id: 2, + value: '1997' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_CONTAINS_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'addresses', + fieldType: 'string', + comparatorType: 'Contains', + dataType: 'user', + id: 2, + value: 'US' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_STARTSWITH_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'addresses', + fieldType: 'string', + comparatorType: 'StartsWith', + dataType: 'user', + id: 2, + value: 'US' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const ARRAY_MATCHREGEX_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '285', + name: 'Criteria_EventTimeStamp_3_Long', + createdAt: 1722497422151, + updatedAt: 1722500235276, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'addresses', + fieldType: 'string', + comparatorType: 'MatchesRegex', + dataType: 'user', + id: 2, + value: '^(JP|DE|GB)' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const NESTED_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '168', + name: 'nested testing', + createdAt: 1721251169153, + updatedAt: 1723488175352, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'furniture', + comparatorType: 'IsSet', + value: '', + fieldType: 'nested' + }, + { + dataType: 'user', + field: 'furniture.furnitureType', + comparatorType: 'Equals', + value: 'Sofa', + fieldType: 'string' + }, + { + dataType: 'user', + field: 'furniture.furnitureColor', + comparatorType: 'Equals', + value: 'White', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const NESTED_CRITERIA_MULTI_LEVEL = { + count: 1, + criteriaSets: [ + { + criteriaId: '425', + name: 'Multi level Nested field criteria', + createdAt: 1721251169153, + updatedAt: 1723488175352, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'button-clicked.browserVisit.website.domain', + comparatorType: 'Equals', + value: 'https://mybrand.com/socks', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const NESTED_CRITERIA_MULTI_LEVEL_ARRAY = { + count: 1, + criteriaSets: [ + { + criteriaId: '436', + name: 'Criteria 2.1 - 09252024 Bug Bash', + createdAt: 1727286807360, + updatedAt: 1727445082036, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'furniture.material.type', + comparatorType: 'Contains', + value: 'table', + fieldType: 'string' + }, + { + dataType: 'user', + field: 'furniture.material.color', + comparatorType: 'Equals', + values: ['black'] + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const NESTED_CRITERIA_MULTI_LEVEL_ARRAY_TRACK_EVENT = { + count: 1, + criteriaSets: [ + { + criteriaId: '459', + name: 'event a.h.b=d && a.h.c=g', + createdAt: 1721251169153, + updatedAt: 1723488175352, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'TopLevelArrayObject.a.h.b', + comparatorType: 'Equals', + value: 'd', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'TopLevelArrayObject.a.h.c', + comparatorType: 'Equals', + value: 'g', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const IS_ONE_OF_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '299', + name: 'Criteria_Is_One_of', + createdAt: 1722851586508, + updatedAt: 1724404229481, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'country', + comparatorType: 'Equals', + values: ['China', 'Japan', 'Kenya'] + }, + { + dataType: 'user', + field: 'addresses', + comparatorType: 'Equals', + values: ['JP', 'DE', 'GB'] + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const IS_NOT_ONE_OF_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '299', + name: 'Criteria_Is_Not_One_of', + createdAt: 1722851586508, + updatedAt: 1724404229481, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'country', + comparatorType: 'DoesNotEqual', + values: ['China', 'Japan', 'Kenya'] + }, + { + dataType: 'user', + field: 'addresses', + comparatorType: 'DoesNotEqual', + values: ['JP', 'DE', 'GB'] + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const CUSTOM_EVENT_API_TEST_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'Equals', + value: 'animal-found', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'animal-found.type', + comparatorType: 'Equals', + value: 'cat', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'animal-found.count', + comparatorType: 'Equals', + value: '6', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'animal-found.vaccinated', + comparatorType: 'Equals', + value: 'true', + fieldType: 'boolean' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const USER_UPDATE_API_TEST_CRITERIA = { + count: 1, + criteriaSets: [ + { + criteriaId: '6', + name: 'UserCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'furniture.furnitureType', + comparatorType: 'Equals', + value: 'Sofa', + fieldType: 'string' + }, + { + dataType: 'user', + field: 'furniture.furnitureColor', + comparatorType: 'Equals', + value: 'White', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const USER_MERGE_SCENARIO_CRITERIA = { + count: 1, + criteriaSets: [ + { + 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: 'testEvent', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +// MARK:Complex Criteria + +export const COMPLEX_CRITERIA_1 = { + count: 1, + criteriaSets: [ + { + criteriaId: '290', + name: 'Complex Criteria Unit Test #1', + createdAt: 1722532861551, + updatedAt: 1722532861551, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'A', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'B', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'C', + fieldType: 'string' + } + ] + } + } + ] + }, + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'IsSet', + value: '', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'saved_cars.color', + comparatorType: 'IsSet', + value: '', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'IsSet', + value: '', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'animal-found.vaccinated', + comparatorType: 'Equals', + value: 'true', + fieldType: 'boolean' + } + ] + } + } + ] + }, + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'total', + comparatorType: 'LessThanOrEqualTo', + value: '100', + fieldType: 'double' + } + ] + } + }, + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'reason', + comparatorType: 'Equals', + value: 'null', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const COMPLEX_CRITERIA_2 = { + count: 1, + criteriaSets: [ + { + criteriaId: '291', + name: 'Complex Criteria Unit Test #2', + createdAt: 1722533473263, + updatedAt: 1722533473263, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Not', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'A', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'B', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'C', + fieldType: 'string' + } + ] + } + } + ] + }, + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + comparatorType: 'IsSet', + value: '', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'saved_cars.color', + comparatorType: 'IsSet', + value: '', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'animal-found.vaccinated', + comparatorType: 'Equals', + value: 'true', + fieldType: 'boolean' + } + ] + } + } + ] + }, + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'total', + comparatorType: 'GreaterThanOrEqualTo', + value: '100', + fieldType: 'double' + } + ] + } + }, + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'reason', + comparatorType: 'DoesNotEqual', + value: 'null', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] +}; + +export const COMPLEX_CRITERIA_3 = { + count: 1, + criteriaSets: [ + { + criteriaId: '292', + name: 'Complex Criteria Unit Test #3', + createdAt: 1722533789589, + updatedAt: 1722533838989, + searchQuery: { + combinator: 'Not', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'A', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'lastName', + comparatorType: 'StartsWith', + value: 'A', + fieldType: 'string' + } + ] + } + } + ] + }, + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'firstName', + comparatorType: 'StartsWith', + value: 'C', + fieldType: 'string' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'animal-found.vaccinated', + comparatorType: 'Equals', + value: 'false', + fieldType: 'boolean' + } + ] + } + }, + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'animal-found.count', + comparatorType: 'LessThan', + value: '5', + fieldType: 'long' + } + ] + } + } + ] + } + ] + } + } + ] +}; diff --git a/src/anonymousUserTracking/tests/criteriaCompletionChecker.test.ts b/src/anonymousUserTracking/tests/criteriaCompletionChecker.test.ts new file mode 100644 index 00000000..931a188c --- /dev/null +++ b/src/anonymousUserTracking/tests/criteriaCompletionChecker.test.ts @@ -0,0 +1,1792 @@ +import { SHARED_PREFS_EVENT_LIST_KEY } from '../../constants'; +import CriteriaCompletionChecker from '../criteriaCompletionChecker'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +describe('CriteriaCompletionChecker', () => { + beforeEach(() => { + (global as any).localStorage = localStorageMock; + }); + + it('should return null if criteriaData is empty', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return '[]'; + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria('{}'); + expect(result).toBeNull(); + }); + + 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: { + browserVisit: { + website: { + domain: 'google.com' + } + } + }, + createNewFields: true, + 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, + criteriaSets: [ + { + 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: '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 customEvent is matched when minMatch present', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'testEvent', + createdAt: 1708494757530, + dataFields: { browserVisit: { website: { domain: 'google.com' } } }, + createNewFields: true, + 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, + criteriaSets: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + minMatch: 1, + maxMatch: 2, + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'eventName', + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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, + criteriaSets: [ + { + 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).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, + criteriaSets: [ + { + 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) { + return JSON.stringify([ + { + items: [ + { + id: '123', + name: 'Black Coffee', + quantity: 1, + price: 4 + } + ], + user: { + userId: 'user' + }, + total: 0, + 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, + criteriaSets: [ + { + criteriaId: '6', + name: 'shoppingCartItemsCriteria', + 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: 'Black Coffee', + fieldType: 'string' + }, + { + dataType: 'purchase', + field: 'shoppingCartItems.price', + comparatorType: 'GreaterThanOrEqualTo', + value: '4.00', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + }); + + it('should return criteriaId if criteriaData condition with StartsWith 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, + createNewFields: true, + 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, + criteriaSets: [ + { + 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: 'StartsWith', + value: 'test', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + + const result1 = checker.getMatchedCriteria( + JSON.stringify({ + count: 1, + criteriaSets: [ + { + 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: 'Contains', + value: 'test', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result1).toEqual('6'); + }); + + it('should return criteriaId if criteria regex match with value is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + email: 'testEvent@example.com', + createdAt: 1708494757530, + dataFields: undefined, + createNewFields: true, + 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, + criteriaSets: [ + { + criteriaId: '6', + name: 'EventCriteria', + createdAt: 1704754280210, + updatedAt: 1704754280210, + searchQuery: { + combinator: 'Or', + searchQueries: [ + { + combinator: 'Or', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'email', + comparatorType: 'MatchesRegex', + value: /^[a-zA-Z0-9]+@(?:[a-zA-Z0-9]+.)+[A-Za-z]+$/, + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('6'); + }); + + // isSet criteria + it('should return criteriaId 97 if isset user criteria is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + country: 'UK', + eventTimeStamp: 10, + phoneNumberDetails: '99999999', + 'shoppingCartItems.price': 50.5 + }, + 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, + criteriaSets: [ + { + criteriaId: '97', + name: 'User', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'IsSet', + dataType: 'user', + id: 25, + value: '' + }, + { + field: 'eventTimeStamp', + fieldType: 'long', + comparatorType: 'IsSet', + dataType: 'user', + id: 26, + valueLong: null, + value: '' + }, + { + field: 'phoneNumberDetails', + fieldType: 'object', + comparatorType: 'IsSet', + dataType: 'user', + id: 28, + value: '' + }, + { + field: 'shoppingCartItems.price', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'user', + id: 30, + value: '' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('97'); + }); + + it('should return null (isset user criteria fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + eventTimeStamp: 10, + phoneNumberDetails: '99999999', + 'shoppingCartItems.price': 50.5 + }, + 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, + criteriaSets: [ + { + criteriaId: '97', + name: 'User', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'country', + fieldType: 'string', + comparatorType: 'IsSet', + dataType: 'user', + id: 25, + value: '' + }, + { + field: 'eventTimeStamp', + fieldType: 'long', + comparatorType: 'IsSet', + dataType: 'user', + id: 26, + valueLong: null, + value: '' + }, + { + field: 'phoneNumberDetails', + fieldType: 'object', + comparatorType: 'IsSet', + dataType: 'user', + id: 28, + value: '' + }, + { + field: 'shoppingCartItems.price', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'user', + id: 30, + value: '' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 94 if isset customEvent criteria is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'button-clicked', + dataFields: { + animal: 'test page', + clickCount: '2', + total: 3 + }, + createdAt: 1700071052507, + 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, + criteriaSets: [ + { + criteriaId: '94', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'button-clicked', + fieldType: 'object', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 2, + value: '' + }, + { + field: 'button-clicked.animal', + fieldType: 'string', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 4, + value: '' + }, + { + field: 'button-clicked.clickCount', + fieldType: 'long', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 5, + valueLong: null, + value: '' + }, + { + field: 'total', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 9, + value: '' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('94'); + }); + + it('should return null (isset customEvent criteria fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'button-clicked', + dataFields: { + 'button-clicked': { animal: 'test page' }, + total: 3 + }, + createdAt: 1700071052507, + 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, + criteriaSets: [ + { + criteriaId: '94', + name: 'Custom Event', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'button-clicked', + fieldType: 'object', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 2, + value: '' + }, + { + field: 'button-clicked.animal', + fieldType: 'string', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 4, + value: '' + }, + { + field: 'button-clicked.clickCount', + fieldType: 'long', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 5, + valueLong: null, + value: '' + }, + { + field: 'total', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'customEvent', + id: 9, + value: '' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 96 if isset purchase criteria is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [ + { + id: '12', + name: 'monitor', + price: 10, + quantity: 10 + } + ], + total: 50, + 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, + criteriaSets: [ + { + criteriaId: '96', + name: 'Purchase', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems', + fieldType: 'object', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 1, + value: '' + }, + { + field: 'shoppingCartItems.price', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 3, + value: '' + }, + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 5, + value: '' + }, + { + field: 'total', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 7, + value: '' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('96'); + }); + + it('should return null (isset purchase criteria fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [ + { + id: '12', + name: 'monitor', + quantity: 10 + } + ], + total: 50, + 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, + criteriaSets: [ + { + criteriaId: '96', + name: 'Purchase', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'shoppingCartItems', + fieldType: 'object', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 1, + value: '' + }, + { + field: 'shoppingCartItems.price', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 3, + value: '' + }, + { + field: 'shoppingCartItems.name', + fieldType: 'string', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 5, + value: '' + }, + { + field: 'total', + fieldType: 'double', + comparatorType: 'IsSet', + dataType: 'purchase', + id: 7, + value: '' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 95 if isset updateCart criteria is matched', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ id: '12', name: 'Mocha', price: 50, quantity: 50 }], + 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, + criteriaSets: [ + { + criteriaId: '95', + name: 'UpdateCart: isSet Comparator', + createdAt: 1719328291857, + updatedAt: 1719328291857, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart', + comparatorType: 'IsSet', + value: '', + fieldType: 'object' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'IsSet', + value: '', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'IsSet', + value: '', + fieldType: 'double' + }, + { + dataType: 'customEvent', + field: + 'updateCart.updatedShoppingCartItems.quantity', + comparatorType: 'IsSet', + value: '', + fieldType: 'long' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('95'); + }); + + it('should return null (isset updateCart criteria fail)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [{ id: '12', name: 'Mocha', quantity: 50 }], + 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, + criteriaSets: [ + { + criteriaId: '95', + name: 'UpdateCart: isSet Comparator', + createdAt: 1719328291857, + updatedAt: 1719328291857, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'customEvent', + field: 'updateCart', + comparatorType: 'IsSet', + value: '', + fieldType: 'object' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.name', + comparatorType: 'IsSet', + value: '', + fieldType: 'string' + }, + { + dataType: 'customEvent', + field: 'updateCart.updatedShoppingCartItems.price', + comparatorType: 'IsSet', + value: '', + fieldType: 'double' + }, + { + dataType: 'customEvent', + field: + 'updateCart.updatedShoppingCartItems.quantity', + comparatorType: 'IsSet', + value: '', + fieldType: 'long' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 100 (boolean test)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + subscribed: true, + phoneNumber: '99999999' + }, + 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, + criteriaSets: [ + { + criteriaId: '100', + name: 'User', + createdAt: 1716560453973, + updatedAt: 1716560453973, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + field: 'subscribed', + fieldType: 'boolean', + comparatorType: 'Equals', + dataType: 'user', + id: 25, + value: 'true' + }, + { + field: 'phoneNumber', + fieldType: 'String', + comparatorType: 'IsSet', + dataType: 'user', + id: 28, + value: '' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('100'); + }); + + it('should return criteriaId 194 if Contact: Phone Number != 57688559', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + subscribed: true, + phoneNumber: '123685748641' + }, + 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, + criteriaSets: [ + { + criteriaId: '194', + name: 'Contact: Phone Number != 57688559', + createdAt: 1721337331194, + updatedAt: 1722338525737, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'phoneNumber', + comparatorType: 'DoesNotEqual', + value: '57688559', + fieldType: 'string' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('194'); + }); + + it('should return criteriaId 293 if Contact: subscribed != false', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + subscribed: true, + phoneNumber: '123685748641' + }, + 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, + criteriaSets: [ + { + criteriaId: '293', + name: 'Contact: subscribed != false', + createdAt: 1722605666776, + updatedAt: 1722606283109, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'user', + field: 'subscribed', + comparatorType: 'DoesNotEqual', + value: 'false', + fieldType: 'boolean' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('293'); + }); + + it('should return criteriaId 297 if Purchase: shoppingCartItems.quantity != 12345678', () => { + (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' + } + ]); + } + 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, + criteriaSets: [ + { + criteriaId: '297', + name: 'Purchase: shoppingCartItems.quantity != 12345678', + createdAt: 1722667099444, + updatedAt: 1722667361286, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.quantity', + comparatorType: 'DoesNotEqual', + value: '12345678', + fieldType: 'long' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('297'); + }); + + it('should return criteriaId 298 if Purchase: shoppingCartItems.price != 105', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + items: [ + { + id: '12', + name: 'monitor', + price: 50.5, + quantity: 10 + } + ], + total: 50, + 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, + criteriaSets: [ + { + criteriaId: '298', + name: 'Purchase: shoppingCartItems.price != 105', + createdAt: 1722606251607, + updatedAt: 1722606295791, + searchQuery: { + combinator: 'And', + searchQueries: [ + { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + searchCombo: { + combinator: 'And', + searchQueries: [ + { + dataType: 'purchase', + field: 'shoppingCartItems.price', + comparatorType: 'DoesNotEqual', + value: '105', + fieldType: 'double' + } + ] + } + } + ] + } + ] + } + } + ] + }) + ); + expect(result).toEqual('298'); + }); +}); diff --git a/src/anonymousUserTracking/tests/dataTypeComparatorSearchQueryCriteria.test.ts b/src/anonymousUserTracking/tests/dataTypeComparatorSearchQueryCriteria.test.ts new file mode 100644 index 00000000..e75cf9f4 --- /dev/null +++ b/src/anonymousUserTracking/tests/dataTypeComparatorSearchQueryCriteria.test.ts @@ -0,0 +1,439 @@ +import { SHARED_PREFS_EVENT_LIST_KEY } from '../../constants'; +import CriteriaCompletionChecker from '../criteriaCompletionChecker'; +import { + DATA_TYPE_COMPARATOR_DOES_NOT_EQUAL, + DATA_TYPE_COMPARATOR_EQUALS, + DATA_TYPE_COMPARATOR_GREATER_THAN, + DATA_TYPE_COMPARATOR_GREATER_THAN_OR_EQUAL_TO, + DATA_TYPE_COMPARATOR_IS_SET, + DATA_TYPE_COMPARATOR_LESS_THAN, + DATA_TYPE_COMPARATOR_LESS_THAN_OR_EQUAL_TO +} from './constants'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +describe('dataTypeComparatorSearchQueryCriteria', () => { + beforeEach(() => { + (global as any).localStorage = localStorageMock; + }); + + it('should return criteriaId 285 (Comparator test For Equal)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 19.99, + likes_boba: true, + country: 'Chaina', + eventTimeStamp: 3 + }, + 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(DATA_TYPE_COMPARATOR_EQUALS) + ); + expect(result).toEqual('285'); + }); + + it('should return null (Comparator test For Equal - No Match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 10.99, + eventTimeStamp: 30, + likes_boba: false, + country: 'Taiwan' + }, + 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(DATA_TYPE_COMPARATOR_EQUALS) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 285 (Comparator test For DoesNotEqual)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 11.2, + eventTimeStamp: 30, + likes_boba: false + }, + 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(DATA_TYPE_COMPARATOR_DOES_NOT_EQUAL) + ); + expect(result).toEqual('285'); + }); + + it('should return null (Comparator test For DoesNotEqual - No Match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 10.99, + eventTimeStamp: 30, + likes_boba: true + }, + 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(DATA_TYPE_COMPARATOR_DOES_NOT_EQUAL) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 289 (Comparator test For LessThan)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 10, + eventTimeStamp: 14 + }, + 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(DATA_TYPE_COMPARATOR_LESS_THAN) + ); + expect(result).toEqual('289'); + }); + + it('should return null (Comparator test For LessThan - No Match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 10, + eventTimeStamp: 18 + }, + 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(DATA_TYPE_COMPARATOR_LESS_THAN) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 290 (Comparator test For LessThanOrEqualTo)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 17, + eventTimeStamp: 14 + }, + 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(DATA_TYPE_COMPARATOR_LESS_THAN_OR_EQUAL_TO) + ); + expect(result).toEqual('290'); + }); + + it('should return null (Comparator test For LessThanOrEqualTo - No Match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 18, + eventTimeStamp: 12 + }, + 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(DATA_TYPE_COMPARATOR_LESS_THAN_OR_EQUAL_TO) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 290 (Comparator test For GreaterThan)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 56, + eventTimeStamp: 51 + }, + 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(DATA_TYPE_COMPARATOR_GREATER_THAN) + ); + expect(result).toEqual('290'); + }); + + it('should return null (Comparator test For GreaterThan - No Match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 5, + eventTimeStamp: 3 + }, + 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(DATA_TYPE_COMPARATOR_GREATER_THAN) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 291 (Comparator test For GreaterThanOrEqualTo)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 20, + eventTimeStamp: 30 + }, + 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(DATA_TYPE_COMPARATOR_GREATER_THAN_OR_EQUAL_TO) + ); + expect(result).toEqual('291'); + }); + + it('should return null (Comparator test For GreaterThanOrEqualTo - No Match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 18, + eventTimeStamp: 16 + }, + 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(DATA_TYPE_COMPARATOR_GREATER_THAN_OR_EQUAL_TO) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 285 (Comparator test For IsSet)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: 10, + eventTimeStamp: 20, + saved_cars: '10', + country: 'Taiwan' + }, + 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(DATA_TYPE_COMPARATOR_IS_SET) + ); + expect(result).toEqual('285'); + }); + + it('should return criteriaId 285 (Comparator test For IsSet - No Match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + savings: '', + eventTimeStamp: '', + saved_cars: 'd', + country: '' + }, + 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(DATA_TYPE_COMPARATOR_IS_SET) + ); + expect(result).toEqual(null); + }); +}); diff --git a/src/anonymousUserTracking/tests/nestedTesting.test.ts b/src/anonymousUserTracking/tests/nestedTesting.test.ts new file mode 100644 index 00000000..be149ffc --- /dev/null +++ b/src/anonymousUserTracking/tests/nestedTesting.test.ts @@ -0,0 +1,416 @@ +import { SHARED_PREFS_EVENT_LIST_KEY } from '../../constants'; +import CriteriaCompletionChecker from '../criteriaCompletionChecker'; +import { + NESTED_CRITERIA, + NESTED_CRITERIA_MULTI_LEVEL, + NESTED_CRITERIA_MULTI_LEVEL_ARRAY, + NESTED_CRITERIA_MULTI_LEVEL_ARRAY_TRACK_EVENT +} from './constants'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +describe('nestedTesting', () => { + beforeEach(() => { + (global as any).localStorage = localStorageMock; + }); + + it('should return criteriaId 168 (nested field)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + email: 'user@example.com', + furniture: [ + { + furnitureType: 'Sofa', + furnitureColor: 'White', + lengthInches: 40, + widthInches: 60 + }, + { + furnitureType: 'table', + furnitureColor: 'Gray', + lengthInches: 20, + widthInches: 30 + } + ] + }, + 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(NESTED_CRITERIA)); + expect(result).toEqual('168'); + }); + + it('should return criteriaId null (nested field - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + email: 'user@example.com', + furniture: [ + { + furnitureType: 'Sofa', + furnitureColor: 'Gray', + lengthInches: 40, + widthInches: 60 + }, + { + furnitureType: 'table', + furnitureColor: 'White', + lengthInches: 20, + widthInches: 30 + } + ] + }, + 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(NESTED_CRITERIA)); + expect(result).toEqual(null); + }); + + it('should return criteriaId 425 (Multi level Nested field criteria)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'button-clicked', + dataFields: { + browserVisit: { website: { domain: 'https://mybrand.com/socks' } } + }, + 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(NESTED_CRITERIA_MULTI_LEVEL) + ); + expect(result).toEqual('425'); + }); + + it('should return criteriaId 425 (Multi level Nested field criteria - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'button-clicked', + dataFields: { + 'button-clicked': { + browserVisit: { + website: { domain: 'https://mybrand.com/socks' } + } + } + }, + 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(NESTED_CRITERIA_MULTI_LEVEL) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId null (Multi level Nested field criteria - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'button-clicked', + dataFields: { + 'browserVisit.website.domain': 'https://mybrand.com/socks' + }, + 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(NESTED_CRITERIA_MULTI_LEVEL) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId null (Multi level Nested field criteria - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'button-clicked', + dataFields: { + browserVisit: { website: { domain: 'https://mybrand.com' } } + }, + 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(NESTED_CRITERIA_MULTI_LEVEL) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId null (Multi level Nested field criteria - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'button-clicked', + dataFields: { + quantity: 11, + domain: 'https://mybrand.com/socks' + }, + 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(NESTED_CRITERIA_MULTI_LEVEL) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 436 (Multi level Nested field criteria)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + furniture: { + material: [ + { + type: 'table', + color: 'black', + lengthInches: 40, + widthInches: 60 + }, + { + type: 'Sofa', + color: 'Gray', + lengthInches: 20, + widthInches: 30 + } + ] + } + }, + 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(NESTED_CRITERIA_MULTI_LEVEL_ARRAY) + ); + expect(result).toEqual('436'); + }); + + it('should return criteriaId null (Multi level Nested field criteria - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + dataFields: { + furniture: { + material: [ + { + type: 'table', + color: 'Gray', + lengthInches: 40, + widthInches: 60 + }, + { + type: 'Sofa', + color: 'black', + lengthInches: 20, + widthInches: 30 + } + ] + } + }, + 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(NESTED_CRITERIA_MULTI_LEVEL_ARRAY) + ); + expect(result).toEqual(null); + }); + + it('should return criteriaId 459 (Multi level Nested field criteria)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'TopLevelArrayObject', + dataFields: { + a: { + h: [ + { + b: 'e', + c: 'h' + }, + { + b: 'd', + c: 'g' + } + ] + } + }, + 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(NESTED_CRITERIA_MULTI_LEVEL_ARRAY_TRACK_EVENT) + ); + expect(result).toEqual('459'); + }); + + it('should return criteriaId null (Multi level Nested field criteria - No match)', () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + eventName: 'TopLevelArrayObject', + dataFields: { + a: { + h: [ + { + b: 'd', + c: 'h' + }, + { + b: 'e', + c: 'g' + } + ] + } + }, + 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(NESTED_CRITERIA_MULTI_LEVEL_ARRAY_TRACK_EVENT) + ); + expect(result).toEqual(null); + }); +}); diff --git a/src/anonymousUserTracking/tests/userMergeScenarios.test.ts b/src/anonymousUserTracking/tests/userMergeScenarios.test.ts new file mode 100644 index 00000000..6d15ad4a --- /dev/null +++ b/src/anonymousUserTracking/tests/userMergeScenarios.test.ts @@ -0,0 +1,817 @@ +import MockAdapter from 'axios-mock-adapter'; +import { initializeWithConfig } from '../../authorization'; +import { + SHARED_PREFS_EVENT_LIST_KEY, + SHARED_PREFS_CRITERIA, + GETMESSAGES_PATH, + ENDPOINT_TRACK_ANON_SESSION, + GET_CRITERIA_PATH, + SHARED_PREFS_ANON_SESSIONS, + ENDPOINT_MERGE_USER, + SHARED_PREF_ANON_USER_ID, + SHARED_PREF_ANON_USAGE_TRACKED +} from '../../constants'; +import { track } from '../../events'; +import { getInAppMessages } from '../../inapp'; +import { baseAxiosRequest } from '../../request'; +import { USER_MERGE_SCENARIO_CRITERIA } from './constants'; + +jest.setTimeout(20000); // Set the timeout to 10 seconds + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +const eventData = { + eventName: 'testEvent123', + dataFields: undefined, + createNewFields: true, + eventType: 'customEvent' +}; + +const eventDataMatched = { + eventName: 'testEvent', + dataFields: undefined, + createNewFields: true, + eventType: 'customEvent' +}; + +const initialAnonSessionInfo = { + itbl_anon_sessions: { + number_of_sessions: 1, + first_session: 123456789, + last_session: expect.any(Number) + } +}; + +declare global { + function uuidv4(): string; + function getEmail(): string; + function getUserID(): string; + function setUserID(): string; +} +const mockRequest = new MockAdapter(baseAxiosRequest); +// const mockOnPostSpy = jest.spyOn(mockRequest, 'onPost'); + +describe('UserMergeScenariosTests', () => { + beforeAll(() => { + (global as any).localStorage = localStorageMock; + global.window = Object.create({ location: { hostname: 'google.com' } }); + mockRequest.onGet(GETMESSAGES_PATH).reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/track').reply(200, {}); + mockRequest.onPost(ENDPOINT_MERGE_USER).reply(200, {}); + mockRequest.onGet(GET_CRITERIA_PATH).reply(200, {}); + mockRequest.onPost(ENDPOINT_TRACK_ANON_SESSION).reply(200, {}); + }); + + beforeEach(() => { + mockRequest.reset(); + mockRequest.resetHistory(); + mockRequest.onGet(GETMESSAGES_PATH).reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/track').reply(200, {}); + mockRequest.onPost(ENDPOINT_MERGE_USER).reply(200, {}); + mockRequest.onGet(GET_CRITERIA_PATH).reply(200, {}); + mockRequest.onPost(ENDPOINT_TRACK_ANON_SESSION).reply(200, {}); + jest.resetAllMocks(); + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventData]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_MERGE_SCENARIO_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + jest.useFakeTimers(); + }); + + describe('UserMergeScenariosTests with setUserID', () => { + it('criteria not met with merge false with setUserId', async () => { + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: false, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent123' }); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setUserID('testuser123'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.userId).toBe('testuser123'); + const removeItemCalls = localStorageMock.removeItem.mock.calls.filter( + (call) => call[0] === SHARED_PREFS_EVENT_LIST_KEY + ); + // count 1 means it did not remove item and so syncEvents was NOT called + // because removeItem gets called one time for the key in case of logout + expect(removeItemCalls.length).toBe(1); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('criteria not met with merge true with setUserId', async () => { + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: true + } + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ ...eventData }); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setUserID('testuser123'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.userId).toBe('testuser123'); + const removeItemCalls = localStorageMock.removeItem.mock.calls.filter( + (call) => call[0] === SHARED_PREFS_EVENT_LIST_KEY + ); + // count 2 means it removed items and so syncEvents was called + + // because removeItem gets called one time for + // the key in case of logout and 2nd time on syncevents + expect(removeItemCalls.length).toBe(2); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('criteria not met with merge default value with setUserId', async () => { + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent123' }); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setUserID('testuser123'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.userId).toBe('testuser123'); + const removeItemCalls = localStorageMock.removeItem.mock.calls.filter( + (call) => call[0] === SHARED_PREFS_EVENT_LIST_KEY + ); + // count 2 means it removed items and so syncEvents was called + + // because removeItem gets called one time for + // the key in case of logout and 2nd time on syncevents + expect(removeItemCalls.length).toBe(2); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('criteria is met with merge false with setUserId', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventDataMatched]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_MERGE_SCENARIO_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + return null; + }); + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log(''); + } + await setUserID('testuser123'); + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.userId).toBe('testuser123'); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('criteria is met with merge true with setUserId', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventDataMatched]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_MERGE_SCENARIO_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: true + } + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + // this function call is needed for putting some delay before executing setUserId + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + console.log(e); + } + await setUserID('testuser123'); + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + jest.useFakeTimers(); + setTimeout(() => { + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeDefined(); // ensure that merge API gets called + }, 1000); + jest.runAllTimers(); + }); + + it('criteria is met with merge default with setUserId', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventDataMatched]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_MERGE_SCENARIO_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USER_ID) { + return '123e4567-e89b-12d3-a456-426614174000'; + } + return null; + }); + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + console.log(e); + } + await setUserID('testuser123'); + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + jest.useFakeTimers(); + setTimeout(() => { + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeDefined(); // ensure that merge API gets called + }, 1000); + jest.runAllTimers(); + }); + + it('current user identified with setUserId merge false', async () => { + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + await setUserID('testuser123'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.userId).toBe('testuser123'); + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + expect(localStorageMock.setItem).not.toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + await setUserID('testuseranotheruser'); + const secondResponse = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(secondResponse.config.params.userId).toBe('testuseranotheruser'); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('current user identified with setUserId merge true', async () => { + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: true + } + } + }); + logout(); // logout to remove logged in users before this test + await setUserID('testuser123'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.userId).toBe('testuser123'); + expect(localStorageMock.setItem).not.toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + await setUserID('testuseranotheruser'); + const secondResponse = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(secondResponse.config.params.userId).toBe('testuseranotheruser'); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeDefined(); // ensure that merge API gets called + }); + + it('current user identified with setUserId merge default', async () => { + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + await setUserID('testuser123'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.userId).toBe('testuser123'); + expect(localStorageMock.setItem).not.toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + await setUserID('testuseranotheruser'); + const secondResponse = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(secondResponse.config.params.userId).toBe('testuseranotheruser'); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + }); + + describe('UserMergeScenariosTests with setEmail', () => { + it('criteria not met with merge false with setEmail', async () => { + const { setEmail, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: false, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent123' }); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setEmail('testuser123@test.com'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.email).toBe('testuser123@test.com'); + const removeItemCalls = localStorageMock.removeItem.mock.calls.filter( + (call) => call[0] === SHARED_PREFS_EVENT_LIST_KEY + ); + // count 1 means it did not remove item and so syncEvents was NOT called + // because removeItem gets called one time for the key in case of logout + expect(removeItemCalls.length).toBe(1); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('criteria not met with merge true with setEmail', async () => { + const { setEmail, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: true + } + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ ...eventData }); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setEmail('testuser123@test.com'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.email).toBe('testuser123@test.com'); + const removeItemCalls = localStorageMock.removeItem.mock.calls.filter( + (call) => call[0] === SHARED_PREFS_EVENT_LIST_KEY + ); + // count 2 means it removed items and so syncEvents was called + + // because removeItem gets called one time for + // the key in case of logout and 2nd time on syncevents + expect(removeItemCalls.length).toBe(2); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('criteria not met with merge default value with setEmail', async () => { + const { setEmail, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent123' }); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setEmail('testuser123@test.com'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.email).toBe('testuser123@test.com'); + const removeItemCalls = localStorageMock.removeItem.mock.calls.filter( + (call) => call[0] === SHARED_PREFS_EVENT_LIST_KEY + ); + // count 2 means it removed items and so syncEvents was called + + // because removeItem gets called one time for + // the key in case of logout and 2nd time on syncevents + expect(removeItemCalls.length).toBe(2); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('criteria is met with merge true with setEmail', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventDataMatched]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_MERGE_SCENARIO_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + const { setEmail } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: true + } + } + }); + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + console.log(e); + } + await setEmail('testuser123@test.com'); + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + jest.useFakeTimers(); + setTimeout(() => { + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeDefined(); // ensure that merge API gets called + }, 1500); + jest.runAllTimers(); + }); + + it('criteria is met with merge default with setEmail', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventDataMatched]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_MERGE_SCENARIO_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USER_ID) { + return '123e4567-e89b-12d3-a456-426614174000'; + } + return null; + }); + const { setEmail, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true + } + }); + logout(); // logout to remove logged in users before this test + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + console.log(e); + } + await setEmail('testuser123@test.com'); + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + jest.useFakeTimers(); + setTimeout(() => { + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeDefined(); // ensure that merge API gets called + }, 1500); + jest.runAllTimers(); + }); + + it('current user identified with setEmail with merge false', async () => { + const { setEmail, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + await setEmail('testuser123@test.com'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.email).toBe('testuser123@test.com'); + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + expect(localStorageMock.setItem).not.toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + await setEmail('testuseranotheruser@test.com'); + const secondResponse = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(secondResponse.config.params.email).toBe( + 'testuseranotheruser@test.com' + ); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + + it('current user identified with setEmail merge true', async () => { + const { setEmail, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: true + } + } + }); + logout(); // logout to remove logged in users before this test + await setEmail('testuser123@test.com'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.email).toBe('testuser123@test.com'); + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + expect(localStorageMock.setItem).not.toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + await setEmail('testuseranotheruser@test.com'); + const secondResponse = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(secondResponse.config.params.email).toBe( + 'testuseranotheruser@test.com' + ); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeDefined(); // ensure that merge API gets called + }); + + it('current user identified with setEmail merge default', async () => { + const { setEmail, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { + enableAnonTracking: true, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: false + } + } + }); + logout(); // logout to remove logged in users before this test + await setEmail('testuser123@test.com'); + const response = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(response.config.params.email).toBe('testuser123@test.com'); + try { + await track({ eventName: 'testEvent' }); + } catch (e) { + console.log('', e); + } + expect(localStorageMock.setItem).not.toHaveBeenCalledWith( + SHARED_PREF_ANON_USER_ID + ); + await setEmail('testuseranotheruser@test.com'); + const secondResponse = await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + expect(secondResponse.config.params.email).toBe( + 'testuseranotheruser@test.com' + ); + const mergePostRequestData = mockRequest.history.post.find( + (req) => req.url === ENDPOINT_MERGE_USER + ); + expect(mergePostRequestData).toBeUndefined(); // ensure that merge API Do NOT get called + }); + }); +}); diff --git a/src/anonymousUserTracking/tests/userUpdate.test.ts b/src/anonymousUserTracking/tests/userUpdate.test.ts new file mode 100644 index 00000000..6d1f84c8 --- /dev/null +++ b/src/anonymousUserTracking/tests/userUpdate.test.ts @@ -0,0 +1,146 @@ +import MockAdapter from 'axios-mock-adapter'; +import { baseAxiosRequest } from '../../request'; +import { + SHARED_PREFS_ANON_SESSIONS, + SHARED_PREFS_EVENT_LIST_KEY, + SHARED_PREFS_CRITERIA, + GET_CRITERIA_PATH, + ENDPOINT_TRACK_ANON_SESSION, + ENDPOINT_MERGE_USER, + SHARED_PREF_ANON_USAGE_TRACKED +} from '../../constants'; +import { updateUser } from '../../users'; +import { initializeWithConfig } from '../../authorization'; +import { CUSTOM_EVENT_API_TEST_CRITERIA } from './constants'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +declare global { + function uuidv4(): string; + function getEmail(): string; + function getUserID(): string; + function setUserID(): string; +} + +const eventDataMatched = { + eventName: 'animal-found', + dataFields: { + type: 'cat', + count: 6, + vaccinated: true + }, + createNewFields: true, + eventType: 'customEvent' +}; + +const userDataMatched = { + dataFields: { + furniture: { + furnitureType: 'Sofa', + furnitureColor: 'White' + } + }, + eventType: 'user' +}; + +const initialAnonSessionInfo = { + itbl_anon_sessions: { + number_of_sessions: 1, + first_session: 123456789, + last_session: expect.any(Number) + } +}; + +const mockRequest = new MockAdapter(baseAxiosRequest); + +describe('UserUpdate', () => { + beforeAll(() => { + (global as any).localStorage = localStorageMock; + global.window = Object.create({ location: { hostname: 'google.com' } }); + mockRequest.onPost('/events/track').reply(200, {}); + mockRequest.onPost('/users/update').reply(200, {}); + mockRequest.onGet(GET_CRITERIA_PATH).reply(200, {}); + mockRequest.onPost(ENDPOINT_TRACK_ANON_SESSION).reply(200, {}); + }); + + beforeEach(() => { + mockRequest.reset(); + mockRequest.resetHistory(); + mockRequest.onPost('/events/track').reply(200, {}); + mockRequest.onPost('/users/update').reply(200, {}); + mockRequest.onPost(ENDPOINT_MERGE_USER).reply(200, {}); + mockRequest.onGet(GET_CRITERIA_PATH).reply(200, {}); + mockRequest.onPost(ENDPOINT_TRACK_ANON_SESSION).reply(200, {}); + jest.resetAllMocks(); + jest.useFakeTimers(); + }); + + it('should not have unnecessary extra nesting when locally stored user update fields are sent to server', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + ...userDataMatched.dataFields, + eventType: userDataMatched.eventType + }, + eventDataMatched + ]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(CUSTOM_EVENT_API_TEST_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + + const { logout } = initializeWithConfig({ + authToken: '123', + configOptions: { enableAnonTracking: true } + }); + logout(); // logout to remove logged in users before this test + + try { + await updateUser(); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + + const trackEvents = mockRequest.history.post.filter( + (req) => req.url === '/anonymoususer/events/session' + ); + + expect(trackEvents.length > 0).toBe(true); + + trackEvents.forEach((req) => { + const requestData = JSON.parse(String(req?.data)); + + expect(requestData).toHaveProperty('user'); + expect(requestData.user).toHaveProperty( + 'dataFields', + userDataMatched.dataFields + ); + expect(requestData.user.dataFields).toHaveProperty( + 'furniture', + userDataMatched.dataFields.furniture + ); + }); + + const trackEventsUserUpdate = mockRequest.history.post.filter( + (req) => req.url === '/users/update' + ); + expect(trackEventsUserUpdate.length === 0).toBe(true); + }); +}); diff --git a/src/anonymousUserTracking/tests/validateCustomEventUserUpdateAPI.test.ts b/src/anonymousUserTracking/tests/validateCustomEventUserUpdateAPI.test.ts new file mode 100644 index 00000000..0acbceb4 --- /dev/null +++ b/src/anonymousUserTracking/tests/validateCustomEventUserUpdateAPI.test.ts @@ -0,0 +1,427 @@ +import MockAdapter from 'axios-mock-adapter'; +import { baseAxiosRequest } from '../../request'; +import { + SHARED_PREFS_ANON_SESSIONS, + SHARED_PREFS_EVENT_LIST_KEY, + SHARED_PREFS_CRITERIA, + ENDPOINT_MERGE_USER, + ENDPOINT_TRACK_ANON_SESSION, + GET_CRITERIA_PATH, + GETMESSAGES_PATH, + SHARED_PREF_ANON_USAGE_TRACKED +} from '../../constants'; +import { track } from '../../events'; +import { initializeWithConfig } from '../../authorization'; +import CriteriaCompletionChecker from '../criteriaCompletionChecker'; +import { updateUser } from '../../users'; +import { + CUSTOM_EVENT_API_TEST_CRITERIA, + USER_UPDATE_API_TEST_CRITERIA +} from './constants'; + +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +}; + +declare global { + function uuidv4(): string; + function getEmail(): string; + function getUserID(): string; + function setUserID(): string; +} + +// SUCCESS +const eventDataMatched = { + eventName: 'animal-found', + dataFields: { + type: 'cat', + count: 6, + vaccinated: true + }, + createNewFields: true, + eventType: 'customEvent' +}; + +// FAIL +const eventData = { + eventName: 'animal-found', + dataFields: { + type: 'cat', + count: 6, + vaccinated: true + }, + type: 'cat', + count: 6, + vaccinated: true, + createNewFields: true, + eventType: 'customEvent' +}; + +// SUCCESS +const userDataMatched = { + dataFields: { + furniture: { + furnitureType: 'Sofa', + furnitureColor: 'White' + } + }, + eventType: 'user' +}; + +// FAIL +const userData = { + dataFields: { + furniture: { + furnitureType: 'Sofa', + furnitureColor: 'White' + } + }, + furnitureType: 'Sofa', + furnitureColor: 'White', + eventType: 'user' +}; + +const initialAnonSessionInfo = { + itbl_anon_sessions: { + number_of_sessions: 1, + first_session: 123456789, + last_session: expect.any(Number) + } +}; + +const mockRequest = new MockAdapter(baseAxiosRequest); + +describe('validateCustomEventUserUpdateAPI', () => { + beforeAll(() => { + (global as any).localStorage = localStorageMock; + global.window = Object.create({ location: { hostname: 'google.com' } }); + mockRequest.onGet(GETMESSAGES_PATH).reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/track').reply(200, {}); + mockRequest.onPost('/users/update').reply(200, {}); + mockRequest.onPost(ENDPOINT_MERGE_USER).reply(200, {}); + mockRequest.onGet(GET_CRITERIA_PATH).reply(200, {}); + mockRequest.onPost(ENDPOINT_TRACK_ANON_SESSION).reply(200, {}); + }); + + beforeEach(() => { + mockRequest.reset(); + mockRequest.resetHistory(); + mockRequest.onGet(GETMESSAGES_PATH).reply(200, { + data: 'something' + }); + mockRequest.onPost('/events/track').reply(200, {}); + mockRequest.onPost('/users/update').reply(200, {}); + mockRequest.onPost(ENDPOINT_MERGE_USER).reply(200, {}); + mockRequest.onGet(GET_CRITERIA_PATH).reply(200, {}); + mockRequest.onPost(ENDPOINT_TRACK_ANON_SESSION).reply(200, {}); + jest.resetAllMocks(); + jest.useFakeTimers(); + }); + + it('should not have unnecessary extra nesting when locally stored user update fields are sent to server', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + ...userDataMatched.dataFields, + eventType: userDataMatched.eventType + } + ]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_UPDATE_API_TEST_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const localStoredCriteriaSets = localStorage.getItem(SHARED_PREFS_CRITERIA); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + + const result = checker.getMatchedCriteria(localStoredCriteriaSets!); + expect(result).toEqual('6'); + + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { enableAnonTracking: true } + }); + logout(); // logout to remove logged in users before this test + + try { + await updateUser(); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setUserID('testuser123'); + + const trackEvents = mockRequest.history.post.filter( + (req) => req.url === '/users/update' + ); + + expect(trackEvents.length > 0).toBe(true); + + trackEvents.forEach((req) => { + const requestData = JSON.parse(String(req?.data)); + + expect(requestData).toHaveProperty( + 'dataFields', + userDataMatched.dataFields + ); + expect(requestData.dataFields).toHaveProperty( + 'furniture', + userDataMatched.dataFields.furniture + ); + expect(requestData.dataFields).toHaveProperty( + 'furniture.furnitureType', + userDataMatched.dataFields.furniture.furnitureType + ); + expect(requestData.dataFields).toHaveProperty( + 'furniture.furnitureColor', + userDataMatched.dataFields.furniture.furnitureColor + ); + + expect(requestData).not.toHaveProperty('furniture'); + expect(requestData).not.toHaveProperty('furnitureType'); + expect(requestData).not.toHaveProperty('furnitureColor'); + expect(requestData).not.toHaveProperty('furniture.furnitureType'); + expect(requestData).not.toHaveProperty('furniture.furnitureColor'); + }); + }); + + it('should not have unnecessary extra nesting when locally stored user update fields are sent to server - Fail', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([ + { + ...userData, + ...userData.dataFields, + eventType: userData.eventType + } + ]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(USER_UPDATE_API_TEST_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const localStoredCriteriaSets = localStorage.getItem(SHARED_PREFS_CRITERIA); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + + const result = checker.getMatchedCriteria(localStoredCriteriaSets!); + expect(result).toEqual('6'); + + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { enableAnonTracking: true } + }); + logout(); // logout to remove logged in users before this test + + try { + await updateUser(); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setUserID('testuser123'); + + const trackEvents = mockRequest.history.post.filter( + (req) => req.url === '/users/update' + ); + + expect(trackEvents.length > 0).toBe(true); + + trackEvents.forEach((req) => { + const requestData = JSON.parse(String(req?.data)); + + expect(requestData).toHaveProperty('dataFields'); + expect(requestData.dataFields).toHaveProperty('furniture'); + expect(requestData.dataFields).toHaveProperty('furniture.furnitureType'); + expect(requestData.dataFields).toHaveProperty('furniture.furnitureColor'); + + expect(requestData).not.toHaveProperty('furniture'); + expect(requestData).not.toHaveProperty('furniture.furnitureType'); + expect(requestData).not.toHaveProperty('furniture.furnitureColor'); + expect(requestData.dataFields).toHaveProperty('furnitureType'); + expect(requestData.dataFields).toHaveProperty('furnitureColor'); + }); + }); + + it('should not have unnecessary extra nesting when locally stored custom event fields are sent to server', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventDataMatched]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(CUSTOM_EVENT_API_TEST_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const localStoredCriteriaSets = localStorage.getItem(SHARED_PREFS_CRITERIA); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + + const result = checker.getMatchedCriteria(localStoredCriteriaSets!); + + expect(result).toEqual('6'); + + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { enableAnonTracking: true } + }); + logout(); // logout to remove logged in users before this test + + try { + await track(eventDataMatched); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setUserID('testuser123'); + + const trackEvents = mockRequest.history.post.filter( + (req) => req.url === '/events/track' + ); + + trackEvents.forEach((req) => { + const requestData = JSON.parse(String(req?.data)); + + expect(requestData).toHaveProperty( + 'eventName', + eventDataMatched.eventName + ); + expect(requestData).toHaveProperty( + 'dataFields', + eventDataMatched.dataFields + ); + + expect(requestData).not.toHaveProperty(eventDataMatched.eventName); + expect(requestData).not.toHaveProperty('type'); + expect(requestData).not.toHaveProperty('count'); + expect(requestData).not.toHaveProperty('vaccinated'); + expect(requestData).not.toHaveProperty('animal-found.type'); + expect(requestData).not.toHaveProperty('animal-found.count'); + expect(requestData).not.toHaveProperty('animal-found.vaccinated'); + }); + }); + + it('should not have unnecessary extra nesting when locally stored custom event fields are sent to server - Fail', async () => { + (localStorage.getItem as jest.Mock).mockImplementation((key) => { + if (key === SHARED_PREFS_EVENT_LIST_KEY) { + return JSON.stringify([eventData]); + } + if (key === SHARED_PREFS_CRITERIA) { + return JSON.stringify(CUSTOM_EVENT_API_TEST_CRITERIA); + } + if (key === SHARED_PREFS_ANON_SESSIONS) { + return JSON.stringify(initialAnonSessionInfo); + } + if (key === SHARED_PREF_ANON_USAGE_TRACKED) { + return 'true'; + } + return null; + }); + + const localStoredEventList = localStorage.getItem( + SHARED_PREFS_EVENT_LIST_KEY + ); + + const localStoredCriteriaSets = localStorage.getItem(SHARED_PREFS_CRITERIA); + + const checker = new CriteriaCompletionChecker( + localStoredEventList === null ? '' : localStoredEventList + ); + const result = checker.getMatchedCriteria( + JSON.stringify(localStoredCriteriaSets) + ); + expect(result).toBeNull(); + + const { setUserID, logout } = initializeWithConfig({ + authToken: '123', + configOptions: { enableAnonTracking: true } + }); + logout(); // logout to remove logged in users before this test + + try { + await track(eventData); + } catch (e) { + console.log(''); + } + expect(localStorage.setItem).toHaveBeenCalledWith( + SHARED_PREFS_EVENT_LIST_KEY, + expect.any(String) + ); + await setUserID('testuser123'); + + const trackEvents = mockRequest.history.post.filter( + (req) => req.url === '/events/track' + ); + + trackEvents.forEach((req) => { + const requestData = JSON.parse(String(req?.data)); + + expect(requestData).toHaveProperty('eventName', eventData.eventName); + expect(requestData).toHaveProperty('dataFields', eventData.dataFields); + + expect(requestData).not.toHaveProperty(eventData.eventName); + expect(requestData).toHaveProperty('type'); + expect(requestData).toHaveProperty('count'); + expect(requestData).toHaveProperty('vaccinated'); + expect(requestData).not.toHaveProperty('animal-found.type'); + expect(requestData).not.toHaveProperty('animal-found.count'); + expect(requestData).not.toHaveProperty('animal-found.vaccinated'); + }); + }); +}); diff --git a/src/authorization/authorization.test.ts b/src/authorization/authorization.test.ts index 84614faa..2e51bef4 100644 --- a/src/authorization/authorization.test.ts +++ b/src/authorization/authorization.test.ts @@ -1,19 +1,21 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { initialize } from './authorization'; +import { initialize, setTypeOfAuthForTestingOnly } from './authorization'; import { baseAxiosRequest } from '../request'; import { getInAppMessages } from '../inapp'; import { track, trackInAppClose } from '../events'; import { updateSubscriptions, updateUser, updateUserEmail } from '../users'; import { trackPurchase, updateCart } from '../commerce'; -import { GETMESSAGES_PATH } from '../constants'; - -let mockRequest: any = null; +import { GETMESSAGES_PATH, INITIALIZE_ERROR } from '../constants'; const localStorageMock = { - setItem: jest.fn() + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() }; +let mockRequest: any = null; + /* decoded payload is: @@ -29,6 +31,7 @@ const MOCK_JWT_KEY_WITH_ONE_MINUTE_EXPIRY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJleHAiOjE2Nzk0ODMyOTEsImlhdCI6MTY3OTQ4MzIzMX0.APaQAYy-lTE0o8rbR6b6-28eCICq36SQMBXmeZAvk1k'; describe('API Key Interceptors', () => { beforeAll(() => { + (global as any).localStorage = localStorageMock; mockRequest = new MockAdapter(baseAxiosRequest); mockRequest.onGet(GETMESSAGES_PATH).reply(200, { data: 'something' @@ -38,6 +41,8 @@ describe('API Key Interceptors', () => { }); beforeEach(() => { + setTypeOfAuthForTestingOnly('userID'); + mockRequest.onPost('/users/update').reply(200, { data: 'something' }); @@ -105,7 +110,7 @@ describe('API Key Interceptors', () => { packageName: 'my-lil-website' }); expect(response.config.headers['Api-Key']).toBe('123'); - expect(response.config.headers['Authorization']).toBe( + expect(response.config.headers.Authorization).toBe( `Bearer ${MOCK_JWT_KEY}` ); }); @@ -122,7 +127,7 @@ describe('API Key Interceptors', () => { packageName: 'my-lil-website' }); expect(response.config.headers['Api-Key']).toBe('123'); - expect(response.config.headers['Authorization']).toBe( + expect(response.config.headers.Authorization).toBe( `Bearer ${MOCK_JWT_KEY}` ); }); @@ -227,8 +232,8 @@ describe('API Key Interceptors', () => { await updateUserEmail('helloworld@gmail.com'); jest.advanceTimersByTime(60000 * 4.1); - /* - called once originally, a second time after the email was changed, + /* + called once originally, a second time after the email was changed, and a third after the JWT was about to expire */ expect(mockGenerateJWT).toHaveBeenCalledTimes(3); @@ -276,8 +281,8 @@ describe('API Key Interceptors', () => { }); jest.advanceTimersByTime(60000 * 4.1); - /* - called once originally, a second time after the email was changed, + /* + called once originally, a second time after the email was changed, and a third after the JWT was about to expire */ expect(mockGenerateJWT).toHaveBeenCalledTimes(3); @@ -310,8 +315,8 @@ describe('API Key Interceptors', () => { }); jest.advanceTimersByTime(60000 * 4.1); - /* - called once originally, a second time after the email was changed, + /* + called once originally, a second time after the email was changed, and a third after the JWT was about to expire */ expect(mockGenerateJWT).toHaveBeenCalledTimes(3); @@ -339,6 +344,8 @@ describe('API Key Interceptors', () => { describe('User Identification', () => { beforeEach(() => { + setTypeOfAuthForTestingOnly('userID'); + /* clear any interceptors already configured */ [ ...Array( @@ -351,6 +358,7 @@ describe('User Identification', () => { describe('non-JWT auth', () => { beforeAll(() => { + (global as any).localStorage = localStorageMock; mockRequest = new MockAdapter(baseAxiosRequest); mockRequest.onPost('/users/update').reply(200, {}); @@ -363,14 +371,17 @@ describe('User Identification', () => { describe('logout', () => { it('logout method removes the email field from requests', async () => { const { logout, setEmail } = initialize('123'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); logout(); - const response = await getInAppMessages({ - count: 10, - packageName: 'my-lil-website' - }); - expect(response.config.params.email).toBeUndefined(); + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + expect(e).toStrictEqual(INITIALIZE_ERROR); + } }); it('logout method removes the userId field from requests', async () => { @@ -379,32 +390,34 @@ describe('User Identification', () => { await setUserID('hello@gmail.com'); logout(); - const response = await getInAppMessages({ - count: 10, - packageName: 'my-lil-website' - }); - expect(response.config.params.userId).toBeUndefined(); + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + expect(e).toStrictEqual(INITIALIZE_ERROR); + } }); }); describe('setEmail', () => { it('adds email param to endpoint that need an email as a param', async () => { const { setEmail } = initialize('123'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); const response = await getInAppMessages({ count: 10, packageName: 'my-lil-website' }); - expect(response.config.params.email).toBe('hello@gmail.com'); }); it('clears any previous interceptors if called twice', async () => { const spy = jest.spyOn(baseAxiosRequest.interceptors.request, 'eject'); const { setEmail } = initialize('123'); - setEmail('hello@gmail.com'); - setEmail('new@gmail.com'); + await setEmail('hello@gmail.com'); + await setEmail('new@gmail.com'); const response = await getInAppMessages({ count: 10, @@ -421,7 +434,7 @@ describe('User Identification', () => { it('adds email body to endpoint that need an email as a body', async () => { const { setEmail } = initialize('123'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); mockRequest.onPost('/events/trackInAppClose').reply(200, { data: 'something' @@ -450,7 +463,7 @@ describe('User Identification', () => { expect(JSON.parse(subsResponse.config.data).email).toBe( 'hello@gmail.com' ); - expect(JSON.parse(userResponse.config.data).email).toBe( + expect(JSON.parse(userResponse && userResponse.config.data).email).toBe( 'hello@gmail.com' ); expect(JSON.parse(trackResponse.config.data).email).toBe( @@ -460,7 +473,7 @@ describe('User Identification', () => { it('adds currentEmail body to endpoint that need an currentEmail as a body', async () => { const { setEmail } = initialize('123'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); mockRequest.onPost('/users/updateEmail').reply(200, { data: 'something' @@ -475,7 +488,7 @@ describe('User Identification', () => { it('should add user.email param to endpoints that need it', async () => { const { setEmail } = initialize('123'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); mockRequest.onPost('/commerce/updateCart').reply(200, { data: 'something' @@ -496,7 +509,7 @@ describe('User Identification', () => { it('adds no email body or header information to unrelated endpoints', async () => { const { setEmail } = initialize('123'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); mockRequest.onPost('/users/hello').reply(200, { data: 'something' @@ -518,7 +531,7 @@ describe('User Identification', () => { it('should overwrite user ID set by setUserID', async () => { const { setEmail, setUserID } = initialize('123'); await setUserID('999'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); const response = await getInAppMessages({ count: 10, @@ -601,7 +614,9 @@ describe('User Identification', () => { expect(JSON.parse(closeResponse.config.data).userId).toBe('999'); expect(JSON.parse(subsResponse.config.data).userId).toBe('999'); - expect(JSON.parse(userResponse.config.data).userId).toBe('999'); + expect( + JSON.parse(userResponse && userResponse.config.data).userId + ).toBe('999'); expect(JSON.parse(trackResponse.config.data).userId).toBe('999'); }); @@ -657,7 +672,7 @@ describe('User Identification', () => { it('should overwrite email set by setEmail', async () => { const { setEmail, setUserID } = initialize('123'); - setEmail('hello@gmail.com'); + await setEmail('hello@gmail.com'); await setUserID('999'); const response = await getInAppMessages({ @@ -667,19 +682,6 @@ describe('User Identification', () => { expect(response.config.params.email).toBeUndefined(); expect(response.config.params.userId).toBe('999'); }); - - it('should try /users/update 0 times if request to create a user fails', async () => { - mockRequest.onPost('/users/update').reply(400, {}); - - const { setUserID } = initialize('123'); - await setUserID('999'); - - expect( - mockRequest.history.post.filter( - (e: any) => !!e.url?.match(/users\/update/gim) - ).length - ).toBe(1); - }); }); }); @@ -702,11 +704,14 @@ describe('User Identification', () => { await setEmail('hello@gmail.com'); logout(); - const response = await getInAppMessages({ - count: 10, - packageName: 'my-lil-website' - }); - expect(response.config.params.email).toBeUndefined(); + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + expect(e).toStrictEqual(INITIALIZE_ERROR); + } }); it('logout method removes the userId field from requests', async () => { @@ -716,11 +721,14 @@ describe('User Identification', () => { await setUserID('hello@gmail.com'); logout(); - const response = await getInAppMessages({ - count: 10, - packageName: 'my-lil-website' - }); - expect(response.config.params.userId).toBeUndefined(); + try { + await getInAppMessages({ + count: 10, + packageName: 'my-lil-website' + }); + } catch (e) { + expect(e).toStrictEqual(INITIALIZE_ERROR); + } }); }); @@ -793,7 +801,7 @@ describe('User Identification', () => { expect(JSON.parse(subsResponse.config.data).email).toBe( 'hello@gmail.com' ); - expect(JSON.parse(userResponse.config.data).email).toBe( + expect(JSON.parse(userResponse && userResponse.config.data).email).toBe( 'hello@gmail.com' ); expect(JSON.parse(trackResponse.config.data).email).toBe( @@ -960,7 +968,9 @@ describe('User Identification', () => { expect(JSON.parse(closeResponse.config.data).userId).toBe('999'); expect(JSON.parse(subsResponse.config.data).userId).toBe('999'); - expect(JSON.parse(userResponse.config.data).userId).toBe('999'); + expect( + JSON.parse(userResponse && userResponse.config.data).userId + ).toBe('999'); expect(JSON.parse(trackResponse.config.data).userId).toBe('999'); }); @@ -1069,7 +1079,7 @@ describe('User Identification', () => { packageName: 'my-lil-website' }); expect(response.config.headers['Api-Key']).toBe('123'); - expect(response.config.headers['Authorization']).toBe( + expect(response.config.headers.Authorization).toBe( `Bearer ${MOCK_JWT_KEY}` ); }); @@ -1086,7 +1096,7 @@ describe('User Identification', () => { packageName: 'my-lil-website' }); expect(response.config.headers['Api-Key']).toBe('123'); - expect(response.config.headers['Authorization']).toBe( + expect(response.config.headers.Authorization).toBe( `Bearer ${MOCK_JWT_KEY}` ); }); @@ -1100,7 +1110,6 @@ describe('User Identification', () => { .mockReturnValue(Promise.resolve(MOCK_JWT_KEY)); const { refreshJwtToken } = initialize('123', mockGenerateJWT); await refreshJwtToken('hello@gmail.com'); - expect(mockGenerateJWT).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(60000 * 4.1); expect(mockGenerateJWT).toHaveBeenCalledTimes(2); diff --git a/src/authorization/authorization.ts b/src/authorization/authorization.ts index 109bf78a..f2d31000 100644 --- a/src/authorization/authorization.ts +++ b/src/authorization/authorization.ts @@ -1,17 +1,16 @@ /* eslint-disable */ import axios from 'axios'; import { baseAxiosRequest } from '../request'; -import { updateUser } from '../users'; -import { clearMessages } from '../inapp'; +import { clearMessages } from 'src/inapp/inapp'; import { IS_PRODUCTION, - RETRY_USER_ATTEMPTS, STATIC_HEADERS, - SHARED_PREF_USER_ID, - SHARED_PREF_EMAIL, + SHARED_PREF_ANON_USER_ID, ENDPOINTS, - RouteConfig -} from '../constants'; + RouteConfig, + SHARED_PREF_ANON_USAGE_TRACKED, + SHARED_PREFS_CRITERIA +} from 'src/constants'; import { cancelAxiosRequestAndMakeFetch, getEpochDifferenceInMS, @@ -21,39 +20,294 @@ import { validateTokenTime, isEmail } from './utils'; -import { Options, config } from '../utils/config'; +import { AnonymousUserMerge } from 'src/anonymousUserTracking/anonymousUserMerge'; +import { + AnonymousUserEventManager, + isAnonymousUsageTracked, + registerAnonUserIdSetter +} from 'src/anonymousUserTracking/anonymousUserEventManager'; +import { IdentityResolution, Options, config } from 'src/utils/config'; +import { getTypeOfAuth, setTypeOfAuth, TypeOfAuth } from 'src/utils/typeOfAuth'; const MAX_TIMEOUT = ONE_DAY; +let authIdentifier: null | string = null; +let userInterceptor: number | null = null; +let apiKey: null | string = null; +let generateJWTGlobal: any = null; +const anonUserManager = new AnonymousUserEventManager(); export interface GenerateJWTPayload { email?: string; userID?: string; } +const doesRequestUrlContain = (routeConfig: RouteConfig) => + Object.entries(ENDPOINTS).some( + (entry) => + routeConfig.route === entry[1].route && + routeConfig.body === entry[1].body && + routeConfig.current === entry[1].current && + routeConfig.nestedUser === entry[1].nestedUser + ); export interface WithJWT { clearRefresh: () => void; setEmail: (email: string) => Promise; setUserID: (userId: string) => Promise; logout: () => void; refreshJwtToken: (authTypes: string) => Promise; + toggleAnonUserTrackingConsent: (consent: boolean) => void; } export interface WithoutJWT { setNewAuthToken: (newToken?: string) => void; clearAuthToken: () => void; - setEmail: (email: string) => void; + setEmail: (email: string) => Promise; setUserID: (userId: string) => Promise; logout: () => void; + toggleAnonUserTrackingConsent: (consent: boolean) => void; } -const doesRequestUrlContain = (routeConfig: RouteConfig) => - Object.entries(ENDPOINTS).some( - (entry) => - routeConfig.route === entry[1].route && - routeConfig.body === entry[1].body && - routeConfig.current === entry[1].current && - routeConfig.nestedUser === entry[1].nestedUser - ); +export const setAnonUserId = async (userId: string) => { + const anonymousUsageTracked = isAnonymousUsageTracked(); + + if (!anonymousUsageTracked) return; + + let token: null | string = null; + if (generateJWTGlobal) { + token = await generateJWTGlobal({ userID: userId }); + } + + baseAxiosRequest.interceptors.request.use((config) => { + config.headers.set('Api-Key', apiKey); + if (token) { + config.headers.set('Authorization', `Bearer ${token}`); + } + return config; + }); + addUserIdToRequest(userId); + localStorage.setItem(SHARED_PREF_ANON_USER_ID, userId); +}; + +registerAnonUserIdSetter(setAnonUserId); + +const clearAnonymousUser = () => { + localStorage.removeItem(SHARED_PREF_ANON_USER_ID); +}; + +const getAnonUserId = () => { + if (config.getConfig('enableAnonTracking')) { + const anonUser = localStorage.getItem(SHARED_PREF_ANON_USER_ID); + return anonUser === undefined ? null : anonUser; + } else { + return null; + } +}; + +const initializeUserId = (userId: string) => { + addUserIdToRequest(userId); + clearAnonymousUser(); +} + +const addUserIdToRequest = (userId: string) => { + setTypeOfAuth('userID'); + authIdentifier = userId; + + if (typeof userInterceptor === 'number') { + baseAxiosRequest.interceptors.request.eject(userInterceptor); + } + /* + endpoints that use _userId_ payload prop in POST/PUT requests + */ + userInterceptor = baseAxiosRequest.interceptors.request.use((config) => { + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: true, + current: true, + nestedUser: true + }) + ) { + return { + ...config, + data: { + ...(config.data || {}), + currentUserId: userId + } + }; + } + + /* + endpoints that use _userId_ payload prop in POST/PUT requests + */ + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: true, + current: false, + nestedUser: false + }) + ) { + return { + ...config, + data: { + ...(config.data || {}), + userId + } + }; + } + + /* + endpoints that use _userId_ payload prop in POST/PUT requests nested in { user: {} } + */ + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: true, + current: false, + nestedUser: true + }) + ) { + return { + ...config, + data: { + ...(config.data || {}), + user: { + ...(config.data.user || {}), + userId + } + } + }; + } + + /* + endpoints that use _userId_ query param in GET requests + */ + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: false, + current: false, + nestedUser: false + }) + ) { + return { + ...config, + params: { + ...(config.params || {}), + userId + } + }; + } + + return config; + }); +}; + +const initializeEmailUser = (email: string) => { + addEmailToRequest(email); + clearAnonymousUser(); +} + +const syncEvents = () => { + if (config.getConfig('enableAnonTracking')) { + anonUserManager.syncEvents(); + } +}; + +const addEmailToRequest = (email: string) => { + setTypeOfAuth('email'); + authIdentifier = email; + + if (typeof userInterceptor === 'number') { + baseAxiosRequest.interceptors.request.eject(userInterceptor); + } + userInterceptor = baseAxiosRequest.interceptors.request.use((config) => { + /* + endpoints that use _currentEmail_ payload prop in POST/PUT requests + */ + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: true, + current: true, + nestedUser: true + }) + ) { + return { + ...config, + data: { + ...(config.data || {}), + currentEmail: email + } + }; + } + + /* + endpoints that use _email_ payload prop in POST/PUT requests + */ + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: true, + current: false, + nestedUser: false + }) + ) { + return { + ...config, + data: { + ...(config.data || {}), + email + } + }; + } + + /* + endpoints that use _userId_ payload prop in POST/PUT requests nested in { user: {} } + */ + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: true, + current: false, + nestedUser: true + }) + ) { + return { + ...config, + data: { + ...(config.data || {}), + user: { + ...(config.data.user || {}), + email + } + } + }; + } + + /* + endpoints that use _email_ query param in GET requests + */ + if ( + doesRequestUrlContain({ + route: config?.url ?? '', + body: false, + current: false, + nestedUser: false + }) + ) { + return { + ...config, + params: { + ...(config.params || {}), + email + } + }; + } + + return config; + }); +}; export function initialize( authToken: string, @@ -64,6 +318,8 @@ export function initialize( authToken: string, generateJWT?: (payload: GenerateJWTPayload) => Promise ) { + apiKey = authToken; + generateJWTGlobal = generateJWT; const logLevel = config.getConfig('logLevel'); if (!generateJWT && IS_PRODUCTION) { /* only let people use non-JWT mode if running the app locally */ @@ -74,36 +330,17 @@ export function initialize( } return; } - - /* + /* only set token interceptor if we're using a non-JWT key. Otherwise, we'll set it later once we generate the JWT */ - let authInterceptor: number | null = generateJWT - ? null - : baseAxiosRequest.interceptors.request.use((config) => { - config.headers.set('Api-Key', authToken); + let authInterceptor: number | null = + baseAxiosRequest.interceptors.request.use((config) => { + config.headers.set('Api-Key', authToken); - return config; - }); - let userInterceptor: number | null = null; + return config; + }); let responseInterceptor: number | null = null; - /* - AKA did the user auth with their email (setEmail) or user ID (setUserID) - - we're going to use this variable for one circumstance - when calling _updateUserEmail_. - Essentially, when we call the Iterable API to update a user's email address and we get a - successful 200 request, we're going to request a new JWT token, since it might need to - be re-signed with the new email address; however, if the customer code never authorized the - user with an email and instead a user ID, we'll just continue to sign the JWT with the user ID. - - This is mainly just a quality-of-life feature, so that the customer's JWT generation code - doesn't _need_ to support email-signed JWTs if they don't want and purely want to issue the - tokens by user ID. - */ - let typeOfAuth: null | 'email' | 'userID' = null; - /* this will be the literal user ID or email they choose to auth with */ - let authIdentifier: null | string = null; /** method that sets a timer one minute before JWT expiration @@ -153,97 +390,58 @@ export function initialize( const handleTokenExpiration = createTokenExpirationTimer(); - const addEmailToRequest = (email: string) => { - userInterceptor = baseAxiosRequest.interceptors.request.use((config) => { - /* - endpoints that use _currentEmail_ payload prop in POST/PUT requests - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: true, - nestedUser: true - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - currentEmail: email - } - }; - } - - /* - endpoints that use _email_ payload prop in POST/PUT requests - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: false, - nestedUser: false - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - email - } - }; - } - - /* - endpoints that use _userId_ payload prop in POST/PUT requests nested in { user: {} } - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: false, - nestedUser: true - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - user: { - ...(config.data.user || {}), - email - } - } - }; + const enableAnonymousTracking = () => { + try { + if (config.getConfig('enableAnonTracking')) { + anonUserManager.getAnonCriteria(); + anonUserManager.updateAnonSession(); + const anonymousUserId = getAnonUserId(); + if (anonymousUserId !== null) { + // This block will restore the anon userID from localstorage + setAnonUserId(anonymousUserId); + } } + } catch (error) { + console.warn(error); + } + }; - /* - endpoints that use _email_ query param in GET requests - */ - - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: false, - current: false, - nestedUser: false - }) - ) { - return { - ...config, - params: { - ...(config.params || {}), - email - } - }; + const tryMergeUser = async ( + emailOrUserId: string, + isEmail: boolean, + merge?: boolean + ): Promise => { + const typeOfAuth = getTypeOfAuth(); + const enableAnonTracking = config.getConfig('enableAnonTracking'); + const sourceUserIdOrEmail = + authIdentifier === null ? getAnonUserId() : authIdentifier; + const sourceUserId = typeOfAuth === 'email' ? null : sourceUserIdOrEmail; + const sourceEmail = typeOfAuth === 'email' ? sourceUserIdOrEmail : null; + const destinationUserId = isEmail ? null : emailOrUserId; + const destinationEmail = isEmail ? emailOrUserId : null; + // This function will try to merge if anon user exists + if ( + (getAnonUserId() !== null || authIdentifier !== null) && + merge && + enableAnonTracking + ) { + const anonymousUserMerge = new AnonymousUserMerge(); + try { + await anonymousUserMerge.mergeUser( + sourceUserId, + sourceEmail, + destinationUserId, + destinationEmail + ); + } catch (error) { + return Promise.reject(`merging failed: ${error}`); } - - return config; - }); + } + return Promise.resolve(true); // promise resolves here because merging is not needed so we setUserID passed via dev }; if (!generateJWT) { + enableAnonymousTracking(); /* we want to set a normal non-JWT enabled API key */ return { setNewAuthToken: (newToken: string) => { @@ -265,147 +463,49 @@ export function initialize( baseAxiosRequest.interceptors.request.eject(authInterceptor); } }, - setEmail: (email: string) => { - typeOfAuth = 'email'; - authIdentifier = email; - localStorage.setItem(SHARED_PREF_EMAIL, email); + setEmail: async (email: string, identityResolution?: IdentityResolution) => { clearMessages(); - if (typeof userInterceptor === 'number') { - baseAxiosRequest.interceptors.request.eject(userInterceptor); + try { + const identityResolutionConfig = config.getConfig('identityResolution'); + const merge = identityResolution?.mergeOnAnonymousToKnown || identityResolutionConfig?.mergeOnAnonymousToKnown; + const replay = identityResolution?.replayOnVisitorToKnown || identityResolutionConfig?.replayOnVisitorToKnown; + + const result = await tryMergeUser(email, true, merge); + if (result) { + initializeEmailUser(email); + if (replay) { + syncEvents(); + } + return Promise.resolve(); + } + } catch (error) { + // here we will not sync events but just bubble up error of merge + return Promise.reject(`merging failed: ${error}`); } - - /* - endpoints that use _currentEmail_ payload prop in POST/PUT requests - */ - addEmailToRequest(email); }, - setUserID: async (userId: string) => { - typeOfAuth = 'userID'; - authIdentifier = userId; - localStorage.setItem(SHARED_PREF_USER_ID, userId); + setUserID: async (userId: string, identityResolution?: IdentityResolution) => { clearMessages(); - - if (typeof userInterceptor === 'number') { - baseAxiosRequest.interceptors.request.eject(userInterceptor); - } - - /* - endpoints that use _currentUserId payload prop in POST/PUT requests nested in { user: {} } - */ - userInterceptor = baseAxiosRequest.interceptors.request.use( - (config) => { - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: true, - nestedUser: true - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - currentUserId: userId - } - }; - } - - /* - endpoints that use _userId_ payload prop in POST/PUT requests - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: false, - nestedUser: false - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - userId - } - }; - } - - /* - endpoints that use _userId_ payload prop in POST/PUT requests nested in { user: {} } - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: false, - nestedUser: true - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - user: { - ...(config.data.user || {}), - userId - } - } - }; - } - - /* - endpoints that use _userId_ query param in GET requests - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: false, - current: false, - nestedUser: false - }) - ) { - return { - ...config, - params: { - ...(config.params || {}), - userId - } - }; + try { + const identityResolutionConfig = config.getConfig('identityResolution'); + const merge = identityResolution?.mergeOnAnonymousToKnown || identityResolutionConfig?.mergeOnAnonymousToKnown; + const replay = identityResolution?.replayOnVisitorToKnown || identityResolutionConfig?.replayOnVisitorToKnown; + + const result = await tryMergeUser(userId, false, merge); + if (result) { + initializeUserId(userId); + if (replay) { + syncEvents(); } - - return config; + return Promise.resolve(); } - ); - - const tryUser = () => { - let createUserAttempts = 0; - - return async function tryUserNTimes(): Promise { - try { - return await updateUser({}); - } catch (e) { - if (createUserAttempts < RETRY_USER_ATTEMPTS) { - createUserAttempts += 1; - return tryUserNTimes(); - } - - return Promise.reject( - `could not create user after ${createUserAttempts} tries` - ); - } - }; - }; - - try { - return await tryUser()(); - } catch (e) { - /* failed to create a new user. Just silently resolve */ - return Promise.resolve(); + } catch (error) { + // here we will not sync events but just bubble up error of merge + return Promise.reject(`merging failed: ${error}`); } }, logout: () => { - typeOfAuth = null; + anonUserManager.removeAnonSessionCriteriaData(); + setTypeOfAuth(null); authIdentifier = null; /* clear fetched in-app messages */ clearMessages(); @@ -419,6 +519,32 @@ export function initialize( /* stop adding JWT to requests */ baseAxiosRequest.interceptors.request.eject(userInterceptor); } + }, + toggleAnonUserTrackingConsent: (consent: boolean) => { + /* if consent is true, we want to clear anon user data and start tracking from point forward */ + if (consent) { + anonUserManager.removeAnonSessionCriteriaData(); + localStorage.removeItem(SHARED_PREFS_CRITERIA); + + localStorage.setItem(SHARED_PREF_ANON_USAGE_TRACKED, 'true'); + enableAnonymousTracking(); + } else { + /* if consent is false, we want to stop tracking and clear anon user data */ + const anonymousUsageTracked = isAnonymousUsageTracked(); + if (anonymousUsageTracked) { + anonUserManager.removeAnonSessionCriteriaData(); + + localStorage.removeItem(SHARED_PREFS_CRITERIA); + localStorage.removeItem(SHARED_PREF_ANON_USER_ID); + localStorage.removeItem(SHARED_PREF_ANON_USAGE_TRACKED); + + setTypeOfAuth(null); + authIdentifier = null; + /* clear fetched in-app messages */ + clearMessages(); + } + localStorage.setItem(SHARED_PREF_ANON_USAGE_TRACKED, 'false'); + } } }; } @@ -493,7 +619,7 @@ export function initialize( const newEmail = JSON.parse(config.config.data)?.newEmail; const payloadToPass = - typeOfAuth === 'email' + getTypeOfAuth() === 'email' ? { email: newEmail } : { userID: authIdentifier! }; @@ -666,7 +792,7 @@ export function initialize( ); return token; }) - .catch((error: any) => { + .catch((error) => { /* clear interceptor */ if (typeof authInterceptor === 'number') { baseAxiosRequest.interceptors.request.eject(authInterceptor); @@ -674,164 +800,87 @@ export function initialize( return Promise.reject(error); }); }; + + enableAnonymousTracking(); return { clearRefresh: () => { /* this will just clear the existing timeout */ handleTokenExpiration(''); }, - setEmail: (email: string) => { - typeOfAuth = 'email'; - authIdentifier = email; - localStorage.setItem(SHARED_PREF_EMAIL, email); + setEmail: async (email: string, identityResolution?: IdentityResolution) => { /* clear previous user */ clearMessages(); - if (typeof userInterceptor === 'number') { - baseAxiosRequest.interceptors.request.eject(userInterceptor); - } - - addEmailToRequest(email); - - return doRequest({ email }).catch((e: any) => { - if (logLevel === 'verbose') { - console.warn( - 'Could not generate JWT after calling setEmail. Please try calling setEmail again.' - ); + try { + const identityResolutionConfig = config.getConfig('identityResolution'); + const merge = identityResolution?.mergeOnAnonymousToKnown || identityResolutionConfig?.mergeOnAnonymousToKnown; + const replay = identityResolution?.replayOnVisitorToKnown || identityResolutionConfig?.replayOnVisitorToKnown; + + const result = await tryMergeUser(email, true, merge); + if (result) { + initializeEmailUser(email); + try { + return doRequest({ email }).then((token) => { + if (replay) { + syncEvents(); + } + return token; + }).catch((e) => { + if (logLevel === 'verbose') { + console.warn( + 'Could not generate JWT after calling setEmail. Please try calling setEmail again.' + ); + } + return Promise.reject(e); + }); + } catch (e) { + /* failed to create a new user. Just silently resolve */ + return Promise.resolve(); + } } - return Promise.reject(e); - }); + } catch (error) { + // here we will not sync events but just bubble up error of merge + return Promise.reject(`merging failed: ${error}`); + } }, - setUserID: async (userId: string) => { - typeOfAuth = 'userID'; - authIdentifier = userId; - localStorage.setItem(SHARED_PREF_USER_ID, userId); + setUserID: async (userId: string, identityResolution?: IdentityResolution) => { clearMessages(); - - if (typeof userInterceptor === 'number') { - baseAxiosRequest.interceptors.request.eject(userInterceptor); - } - - /* - endpoints that use _currentUserId_ payload prop in POST/PUT requests nested in user object - */ - userInterceptor = baseAxiosRequest.interceptors.request.use((config) => { - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: true, - nestedUser: true - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - currentUserId: userId - } - }; - } - - /* - endpoints that use _serId_ payload prop in POST/PUT requests - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: false, - nestedUser: false - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - userId - } - }; - } - - /* - endpoints that use _userId_ payload prop in POST/PUT requests nested in { user: {} } - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: true, - current: false, - nestedUser: true - }) - ) { - return { - ...config, - data: { - ...(config.data || {}), - user: { - ...(config.data.user || {}), - userId - } - } - }; - } - - /* - endpoints that use _userId_ query param in GET requests - */ - if ( - doesRequestUrlContain({ - route: config?.url ?? '', - body: false, - current: false, - nestedUser: false - }) - ) { - return { - ...config, - params: { - ...(config.params || {}), - userId - } - }; - } - - return config; - }); - - const tryUser = () => { - let createUserAttempts = 0; - - return async function tryUserNTimes(): Promise { + try { + const identityResolutionConfig = config.getConfig('identityResolution'); + const merge = identityResolution?.mergeOnAnonymousToKnown || identityResolutionConfig?.mergeOnAnonymousToKnown; + const replay = identityResolution?.replayOnVisitorToKnown || identityResolutionConfig?.replayOnVisitorToKnown; + + const result = await tryMergeUser(userId, false, merge); + if (result) { + initializeUserId(userId); try { - return await updateUser({}); + return doRequest({ userID: userId }) + .then(async (token) => { + if (replay) { + syncEvents(); + } + return token; + }) + .catch((e) => { + if (logLevel === 'verbose') { + console.warn( + 'Could not generate JWT after calling setUserID. Please try calling setUserID again.' + ); + } + return Promise.reject(e); + }); } catch (e) { - if (createUserAttempts < RETRY_USER_ATTEMPTS) { - createUserAttempts += 1; - return tryUserNTimes(); - } - - return Promise.reject( - `could not create user after ${createUserAttempts} tries` - ); + /* failed to create a new user. Just silently resolve */ + return Promise.resolve(); } - }; - }; - - return doRequest({ userID: userId }) - .then(async (token) => { - await tryUser()(); - return token; - }) - .catch((e: any) => { - if (logLevel === 'verbose') { - console.warn( - 'Could not generate JWT after calling setUserID. Please try calling setUserID again.' - ); - } - return Promise.reject(e); - }); + } + } catch (error) { + // here we will not sync events but just bubble up error of merge + return Promise.reject(`merging failed: ${error}`); + } }, logout: () => { - typeOfAuth = null; + anonUserManager.removeAnonSessionCriteriaData(); + setTypeOfAuth(null); authIdentifier = null; /* clear fetched in-app messages */ clearMessages(); @@ -853,12 +902,38 @@ export function initialize( /* this will just clear the existing timeout */ handleTokenExpiration(''); const payloadToPass = { [isEmail(user) ? 'email' : 'userID']: user }; - return doRequest(payloadToPass).catch((e: any) => { + return doRequest(payloadToPass).catch((e) => { if (logLevel === 'verbose') { console.warn(e); console.warn('Could not refresh JWT. Try Refresh the JWT again.'); } }); + }, + toggleAnonUserTrackingConsent: (consent: boolean) => { + /* if consent is true, we want to clear anon user data and start tracking from point forward */ + if (consent) { + anonUserManager.removeAnonSessionCriteriaData(); + localStorage.removeItem(SHARED_PREFS_CRITERIA); + + localStorage.setItem(SHARED_PREF_ANON_USAGE_TRACKED, 'true'); + enableAnonymousTracking(); + } else { + /* if consent is false, we want to stop tracking and clear anon user data */ + const anonymousUsageTracked = isAnonymousUsageTracked(); + if (anonymousUsageTracked) { + anonUserManager.removeAnonSessionCriteriaData(); + + localStorage.removeItem(SHARED_PREFS_CRITERIA); + localStorage.removeItem(SHARED_PREF_ANON_USER_ID); + localStorage.removeItem(SHARED_PREF_ANON_USAGE_TRACKED); + + setTypeOfAuth(null); + authIdentifier = null; + /* clear fetched in-app messages */ + clearMessages(); + } + localStorage.setItem(SHARED_PREF_ANON_USAGE_TRACKED, 'false'); + } } }; } @@ -893,3 +968,11 @@ export function initializeWithConfig(initializeParams: InitializeParams) { ? initialize(authToken, generateJWT) : initialize(authToken); } + +export function setTypeOfAuthForTestingOnly(authType: TypeOfAuth) { + if (!authType) { + setTypeOfAuth(null); + } else { + setTypeOfAuth(authType); + } +} diff --git a/src/commerce/commerce.test.ts b/src/commerce/commerce.test.ts index 24645400..3f20b023 100644 --- a/src/commerce/commerce.test.ts +++ b/src/commerce/commerce.test.ts @@ -3,10 +3,14 @@ import { baseAxiosRequest } from '../request'; import { trackPurchase, updateCart } from './commerce'; // import { SDK_VERSION, WEB_PLATFORM } from '../constants'; import { createClientError } from '../utils/testUtils'; +import { setTypeOfAuthForTestingOnly } from '../authorization'; const mockRequest = new MockAdapter(baseAxiosRequest); describe('Users Requests', () => { + beforeEach(() => { + setTypeOfAuthForTestingOnly('email'); + }); it('should set params and return the correct payload for updateCart', async () => { mockRequest.onPost('/commerce/updateCart').reply(200, { msg: 'hello' diff --git a/src/commerce/commerce.ts b/src/commerce/commerce.ts index de8a6957..523568d8 100644 --- a/src/commerce/commerce.ts +++ b/src/commerce/commerce.ts @@ -1,9 +1,11 @@ /* eslint-disable no-param-reassign */ -import { ENDPOINTS } from '../constants'; +import { INITIALIZE_ERROR, ENDPOINTS } from '../constants'; import { baseIterableRequest } from '../request'; import { TrackPurchaseRequestParams, UpdateCartRequestParams } from './types'; import { IterableResponse } from '../types'; import { updateCartSchema, trackPurchaseSchema } from './commerce.schema'; +import { AnonymousUserEventManager } from '../anonymousUserTracking/anonymousUserEventManager'; +import { canTrackAnonUser } from '../utils/commonFunctions'; export const updateCart = (payload: UpdateCartRequestParams) => { /* a customer could potentially send these up if they're not using TypeScript */ @@ -11,6 +13,11 @@ export const updateCart = (payload: UpdateCartRequestParams) => { delete (payload as any).user.userId; delete (payload as any).user.email; } + if (canTrackAnonUser()) { + const anonymousUserEventManager = new AnonymousUserEventManager(); + anonymousUserEventManager.trackAnonUpdateCart(payload); + return Promise.reject(INITIALIZE_ERROR); + } return baseIterableRequest({ method: 'POST', @@ -34,6 +41,11 @@ export const trackPurchase = (payload: TrackPurchaseRequestParams) => { delete (payload as any).user.userId; delete (payload as any).user.email; } + if (canTrackAnonUser()) { + const anonymousUserEventManager = new AnonymousUserEventManager(); + anonymousUserEventManager.trackAnonPurchaseEvent(payload); + return Promise.reject(INITIALIZE_ERROR); + } return baseIterableRequest({ method: 'POST', diff --git a/src/constants.ts b/src/constants.ts index 59646f0a..673be044 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,9 @@ export const DISPLAY_INTERVAL_DEFAULT = 30000; /* how many times we try to create a new user when _setUserID_ is invoked */ export const RETRY_USER_ATTEMPTS = 0; +/* How many events can be stored in the local storage */ +export const DEFAULT_EVENT_THRESHOLD_LIMIT = 100; + const IS_EU_ITERABLE_SERVICE = process.env.IS_EU_ITERABLE_SERVICE === 'true'; export const dangerouslyAllowJsPopupExecution = @@ -23,6 +26,9 @@ export const EU_ITERABLE_API = `https://${EU_ITERABLE_DOMAIN}/api`; export const BASE_URL = process.env.BASE_URL || ITERABLE_API_URL; export const GETMESSAGES_PATH = '/inApp/web/getMessages'; +export const GET_CRITERIA_PATH = '/anonymoususer/list'; +export const ENDPOINT_MERGE_USER = '/users/merge'; +export const ENDPOINT_TRACK_ANON_SESSION = '/anonymoususer/events/session'; const GET_ENABLE_INAPP_CONSUME = () => { try { @@ -273,3 +279,34 @@ export const ANIMATION_STYLESHEET = ( transition: visibility 0s ${animationDuration}ms, opacity ${animationDuration}ms linear; } `; + +export const SHARED_PREFS_EVENT_TYPE = 'eventType'; +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_ANON_USAGE_TRACKED = 'itbl_anonymous_usage_tracked'; + +export const KEY_EVENT_NAME = 'eventName'; +export const KEY_CREATED_AT = 'createdAt'; +export const KEY_DATA_FIELDS = 'dataFields'; +export const KEY_CREATE_NEW_FIELDS = 'createNewFields'; +export const KEY_ITEMS = 'items'; +export const KEY_TOTAL = 'total'; +export const KEY_PREFER_USERID = 'preferUserId'; +export const DATA_REPLACE = 'dataReplace'; + +export const TRACK_EVENT = 'customEvent'; +export const TRACK_PURCHASE = 'purchase'; +export const UPDATE_USER = 'user'; +export const TRACK_UPDATE_CART = 'cartUpdate'; +export const UPDATE_CART = 'updateCart'; + +export const PURCHASE_ITEM = 'shoppingCartItems'; +export const UPDATECART_ITEM_PREFIX = 'updateCart.updatedShoppingCartItems.'; +export const PURCHASE_ITEM_PREFIX = `${PURCHASE_ITEM}.`; + +export const MERGE_SUCCESSFULL = 'MERGE_SUCCESSFULL'; +export const INITIALIZE_ERROR = new Error( + 'Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods' +); diff --git a/src/embedded/embeddedManager.test.ts b/src/embedded/embeddedManager.test.ts index 904f1d2d..724a5890 100644 --- a/src/embedded/embeddedManager.test.ts +++ b/src/embedded/embeddedManager.test.ts @@ -1,4 +1,5 @@ import { IterableEmbeddedManager } from './embeddedManager'; +import { setTypeOfAuthForTestingOnly } from '../authorization'; // Mock the baseIterableRequest function jest.mock('../request', () => ({ @@ -11,6 +12,9 @@ jest.mock('..', () => ({ })); describe('EmbeddedManager', () => { + beforeEach(() => { + setTypeOfAuthForTestingOnly('email'); + }); const appPackageName = 'my-website'; describe('syncMessages', () => { it('should call syncMessages and callback', async () => { @@ -28,7 +32,8 @@ describe('EmbeddedManager', () => { const embeddedManager = new IterableEmbeddedManager(appPackageName); async function mockTest() { - return new Promise(function (resolve, reject) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line prefer-promise-reject-errors reject('Invalid API Key'); }); } diff --git a/src/events/events.test.ts b/src/events/events.test.ts index 7c3eac95..6bc13844 100644 --- a/src/events/events.test.ts +++ b/src/events/events.test.ts @@ -13,8 +13,9 @@ import { trackInAppDelivery, trackInAppOpen } from './inapp/events'; -import { WEB_PLATFORM } from '../constants'; +import { INITIALIZE_ERROR, WEB_PLATFORM } from '../constants'; import { createClientError } from '../utils/testUtils'; +import { setTypeOfAuthForTestingOnly } from '../authorization'; const mockRequest = new MockAdapter(baseAxiosRequest); const localStorageMock = { @@ -60,6 +61,10 @@ describe('Events Requests', () => { }); }); + beforeEach(() => { + setTypeOfAuthForTestingOnly('userID'); + }); + it('return the correct payload for track', async () => { const response = await track({ eventName: 'test' }); @@ -258,6 +263,54 @@ describe('Events Requests', () => { ); } }); + it('should fail trackInAppOpen if not authed', async () => { + try { + setTypeOfAuthForTestingOnly(null); + await trackInAppOpen({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); + it('should fail trackInAppClose if not authed', async () => { + try { + setTypeOfAuthForTestingOnly(null); + await trackInAppClose({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); + it('should fail trackInAppClick if not authed', async () => { + try { + setTypeOfAuthForTestingOnly(null); + await trackInAppClick({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); + it('should fail trackInAppConsume if not authed', async () => { + try { + setTypeOfAuthForTestingOnly(null); + await trackInAppConsume({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); + it('should fail trackInAppDelivery if not authed', async () => { + try { + setTypeOfAuthForTestingOnly(null); + await trackInAppDelivery({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); + it('should fail trackInAppOpen if not authed', async () => { + try { + setTypeOfAuthForTestingOnly(null); + await trackInAppOpen({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); it('return the correct payload for embedded message received', async () => { const response = await trackEmbeddedReceived('abc123', 'packageName'); @@ -477,4 +530,31 @@ describe('Events Requests', () => { expect(JSON.parse(trackSessionResponse.config.data).email).toBeUndefined(); expect(JSON.parse(trackSessionResponse.config.data).userId).toBeUndefined(); }); + + it('should fail if no auth type set for embedded received', async () => { + setTypeOfAuthForTestingOnly(null); + try { + await trackEmbeddedReceived('abc123', 'packageName'); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); + + it('should fail if no auth type set for embedded click', async () => { + setTypeOfAuthForTestingOnly(null); + try { + await trackEmbeddedClick({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); + + it('should fail if no auth type set for embedded session', async () => { + setTypeOfAuthForTestingOnly(null); + try { + await trackEmbeddedSession({} as any); + } catch (e) { + expect(e).toBe(INITIALIZE_ERROR); + } + }); }); diff --git a/src/events/events.ts b/src/events/events.ts index 34f65075..486cb61f 100644 --- a/src/events/events.ts +++ b/src/events/events.ts @@ -1,15 +1,21 @@ /* eslint-disable no-param-reassign */ -import { ENDPOINTS } from '../constants'; +import { INITIALIZE_ERROR, ENDPOINTS } from '../constants'; import { baseIterableRequest } from '../request'; import { InAppTrackRequestParams } from './inapp/types'; import { IterableResponse } from '../types'; import { trackSchema } from './events.schema'; +import { AnonymousUserEventManager } from '../anonymousUserTracking/anonymousUserEventManager'; +import { canTrackAnonUser } from '../utils/commonFunctions'; export const track = (payload: InAppTrackRequestParams) => { /* a customer could potentially send these up if they're not using TypeScript */ delete (payload as any).userId; delete (payload as any).email; - + if (canTrackAnonUser()) { + const anonymousUserEventManager = new AnonymousUserEventManager(); + anonymousUserEventManager.trackAnonEvent(payload); + return Promise.reject(INITIALIZE_ERROR); + } return baseIterableRequest({ method: 'POST', url: ENDPOINTS.event_track.route, diff --git a/src/inapp/inapp.ts b/src/inapp/inapp.ts index 21176ce4..cacc7695 100644 --- a/src/inapp/inapp.ts +++ b/src/inapp/inapp.ts @@ -17,7 +17,7 @@ import { trackInAppClose, trackInAppConsume, trackInAppOpen -} from '../events'; +} from '../events/inapp/events'; import { IterablePromise } from '../types'; import { requestMessages } from './request'; import { diff --git a/src/inapp/tests/inapp.test.ts b/src/inapp/tests/inapp.test.ts index 5c40c7bc..86793a72 100644 --- a/src/inapp/tests/inapp.test.ts +++ b/src/inapp/tests/inapp.test.ts @@ -3,7 +3,7 @@ */ import MockAdapter from 'axios-mock-adapter'; import { messages } from '../../__data__/inAppMessages'; -import { initialize } from '../../authorization'; +import { initialize, setTypeOfAuthForTestingOnly } from '../../authorization'; import { GETMESSAGES_PATH, SDK_VERSION, WEB_PLATFORM } from '../../constants'; import { baseAxiosRequest } from '../../request'; import { createClientError } from '../../utils/testUtils'; @@ -20,6 +20,7 @@ describe('getInAppMessages', () => { beforeEach(() => { jest.clearAllMocks(); jest.resetAllMocks(); + setTypeOfAuthForTestingOnly('email'); mockRequest.resetHistory(); mockRequest.onGet(GETMESSAGES_PATH).reply(200, { diff --git a/src/inapp/tests/utils.test.ts b/src/inapp/tests/utils.test.ts index c80633e9..00778d42 100644 --- a/src/inapp/tests/utils.test.ts +++ b/src/inapp/tests/utils.test.ts @@ -19,6 +19,7 @@ import { sortInAppMessages, trackMessagesDelivered } from '../utils'; +import { setTypeOfAuthForTestingOnly } from '../../authorization'; jest.mock('../../utils/srSpeak', () => ({ srSpeak: jest.fn() @@ -33,6 +34,9 @@ const mockMarkup = ` `; describe('Utils', () => { + beforeEach(() => { + setTypeOfAuthForTestingOnly('email'); + }); describe('filterHiddenInAppMessages', () => { it('should filter out read messages', () => { expect(filterHiddenInAppMessages()).toEqual([]); @@ -869,6 +873,7 @@ describe('Utils', () => { expect(el.getAttribute('aria-label')).toBe('hello'); expect(el.getAttribute('role')).toBe('button'); + // eslint-disable-next-line no-script-url expect(el.getAttribute('href')).toBe('javascript:undefined'); }); diff --git a/src/request.ts b/src/request.ts index 8badc433..53ff86ac 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,9 +1,18 @@ import Axios, { AxiosRequestConfig } from 'axios'; import qs from 'qs'; import { AnySchema, ValidationError } from 'yup'; -import { BASE_URL, STATIC_HEADERS, EU_ITERABLE_API } from './constants'; +import { + BASE_URL, + STATIC_HEADERS, + EU_ITERABLE_API, + GET_CRITERIA_PATH, + INITIALIZE_ERROR, + ENDPOINT_MERGE_USER, + ENDPOINT_TRACK_ANON_SESSION +} from './constants'; import { IterablePromise, IterableResponse } from './types'; import { config } from './utils/config'; +import { getTypeOfAuth } from './utils/typeOfAuth'; interface ExtendedRequestConfig extends AxiosRequestConfig { validation?: { @@ -20,6 +29,12 @@ interface ClientError extends IterableResponse { }[]; } +const ENDPOINTS_REQUIRING_SET_USER = [ + GET_CRITERIA_PATH, + ENDPOINT_MERGE_USER, + ENDPOINT_TRACK_ANON_SESSION +]; + export const baseAxiosRequest = Axios.create({ baseURL: BASE_URL }); @@ -28,6 +43,15 @@ export const baseIterableRequest = ( payload: ExtendedRequestConfig ): IterablePromise => { try { + const endpoint = payload?.url ?? ''; + + // for most Iterable API endpoints, we require a user to be initialized in the SDK. + if ( + !ENDPOINTS_REQUIRING_SET_USER.includes(endpoint) && + getTypeOfAuth() === null + ) { + return Promise.reject(INITIALIZE_ERROR); + } if (payload.validation?.data && payload.data) { payload.validation.data.validateSync(payload.data, { abortEarly: false }); } diff --git a/src/users/types.ts b/src/users/types.ts index d0f7c5c8..5cafc406 100644 --- a/src/users/types.ts +++ b/src/users/types.ts @@ -12,10 +12,14 @@ export interface GetUserResponse { export interface UpdateUserParams { dataFields?: Record; - preferUserId?: boolean; mergeNestedObjects?: boolean; } +export interface UpdateAnonymousUserParams extends UpdateUserParams { + createNewFields?: boolean; + userId?: string; +} + export interface UpdateSubscriptionParams { emailListIds: number[]; unsubscribedChannelIds: number[]; diff --git a/src/users/users.schema.ts b/src/users/users.schema.ts index 91e0443f..4557a18a 100644 --- a/src/users/users.schema.ts +++ b/src/users/users.schema.ts @@ -1,6 +1,7 @@ -import { array, boolean, number, object } from 'yup'; +import { array, boolean, number, object, string } from 'yup'; export const updateUserSchema = object().shape({ + userId: string(), dataFields: object(), preferUserId: boolean(), mergeNestedObjects: boolean() diff --git a/src/users/users.test.ts b/src/users/users.test.ts index 3ec297c9..b4cfc9a5 100644 --- a/src/users/users.test.ts +++ b/src/users/users.test.ts @@ -2,11 +2,15 @@ import MockAdapter from 'axios-mock-adapter'; import { baseAxiosRequest } from '../request'; import { updateSubscriptions, updateUser, updateUserEmail } from './users'; import { createClientError } from '../utils/testUtils'; +import { setTypeOfAuthForTestingOnly } from '../authorization'; // import { SDK_VERSION, WEB_PLATFORM } from '../constants'; const mockRequest = new MockAdapter(baseAxiosRequest); describe('Users Requests', () => { + beforeEach(() => { + setTypeOfAuthForTestingOnly('email'); + }); it('should set params and return the correct payload for updateUser', async () => { mockRequest.onPost('/users/update').reply(200, { msg: 'hello' @@ -16,11 +20,13 @@ describe('Users Requests', () => { dataFields: {} }); - expect(JSON.parse(response.config.data).dataFields).toEqual({}); - expect(JSON.parse(response.config.data).preferUserId).toBe(true); + expect(JSON.parse(response && response.config.data).dataFields).toEqual({}); + expect(JSON.parse(response && response.config.data).preferUserId).toBe( + true + ); // expect(response.config.headers['SDK-Version']).toBe(SDK_VERSION); // expect(response.config.headers['SDK-Platform']).toBe(WEB_PLATFORM); - expect(response.data.msg).toBe('hello'); + expect(response && response.data.msg).toBe('hello'); }); it('should reject updateUser on bad params', async () => { diff --git a/src/users/users.ts b/src/users/users.ts index 279a56a9..538637d7 100644 --- a/src/users/users.ts +++ b/src/users/users.ts @@ -1,10 +1,12 @@ // eslint-disable @typescript-eslint/no-explicit-any import { object, string } from 'yup'; -import { ENDPOINTS } from '../constants'; import { IterableResponse } from '../types'; import { baseIterableRequest } from '../request'; import { UpdateSubscriptionParams, UpdateUserParams } from './types'; import { updateSubscriptionsSchema, updateUserSchema } from './users.schema'; +import { AnonymousUserEventManager } from '../anonymousUserTracking/anonymousUserEventManager'; +import { canTrackAnonUser } from '../utils/commonFunctions'; +import { INITIALIZE_ERROR, ENDPOINTS } from '../constants'; export const updateUserEmail = (newEmail: string) => baseIterableRequest({ @@ -26,6 +28,11 @@ export const updateUser = (payloadParam: UpdateUserParams = {}) => { delete (payload as any).userId; delete (payload as any).email; + if (canTrackAnonUser()) { + const anonymousUserEventManager = new AnonymousUserEventManager(); + anonymousUserEventManager.trackAnonUpdateUser(payload); + return Promise.reject(INITIALIZE_ERROR); + } return baseIterableRequest({ method: 'POST', url: ENDPOINTS.users_update.route, diff --git a/src/utils/commonFunctions.ts b/src/utils/commonFunctions.ts new file mode 100644 index 00000000..ecf94eab --- /dev/null +++ b/src/utils/commonFunctions.ts @@ -0,0 +1,5 @@ +import config from './config'; +import { getTypeOfAuth } from './typeOfAuth'; + +export const canTrackAnonUser = (): boolean => + config.getConfig('enableAnonTracking') && getTypeOfAuth() === null; diff --git a/src/utils/config.ts b/src/utils/config.ts index 491314e9..5cc7e65e 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,18 +1,33 @@ -import { BASE_URL } from '../constants'; +import { BASE_URL, DEFAULT_EVENT_THRESHOLD_LIMIT } from '../constants'; + +export type IdentityResolution = { + replayOnVisitorToKnown?: boolean; + mergeOnAnonymousToKnown?: boolean; +}; export type Options = { logLevel: 'none' | 'verbose'; baseURL: string; + enableAnonTracking: boolean; isEuIterableService: boolean; dangerouslyAllowJsPopups: boolean; + eventThresholdLimit?: number; + onAnonUserCreated?: (userId: string) => void; + identityResolution?: IdentityResolution; }; const _config = () => { let options: Options = { logLevel: 'none', baseURL: BASE_URL, + enableAnonTracking: false, isEuIterableService: false, - dangerouslyAllowJsPopups: false + dangerouslyAllowJsPopups: false, + eventThresholdLimit: DEFAULT_EVENT_THRESHOLD_LIMIT, + identityResolution: { + replayOnVisitorToKnown: true, + mergeOnAnonymousToKnown: true + } }; const getConfig = (option: K) => options[option]; @@ -22,7 +37,11 @@ const _config = () => { setConfig: (newOptions: Partial) => { options = { ...options, - ...newOptions + ...newOptions, + identityResolution: { + ...options.identityResolution, + ...newOptions.identityResolution + } }; } }; diff --git a/src/utils/typeOfAuth.ts b/src/utils/typeOfAuth.ts new file mode 100644 index 00000000..38092020 --- /dev/null +++ b/src/utils/typeOfAuth.ts @@ -0,0 +1,24 @@ +/* eslint-disable import/no-mutable-exports */ + +/* + AKA did the user auth with their email (setEmail) or user ID (setUserID) + + we're going to use this variable for one circumstance - when calling _updateUserEmail_. + Essentially, when we call the Iterable API to update a user's email address and we get a + successful 200 request, we're going to request a new JWT token, since it might need to + be re-signed with the new email address; however, if the customer code never authorized the + user with an email and instead a user ID, we'll just continue to sign the JWT with the user ID. + + This is mainly just a quality-of-life feature, so that the customer's JWT generation code + doesn't _need_ to support email-signed JWTs if they don't want and purely want to issue the + tokens by user ID. + */ +/* this will be the literal user ID or email they choose to auth with */ + +export type TypeOfAuth = null | 'email' | 'userID'; +let typeOfAuth: TypeOfAuth = null; +export const setTypeOfAuth = (value: TypeOfAuth) => { + typeOfAuth = value; +}; + +export const getTypeOfAuth = () => typeOfAuth; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 00000000..656aadeb --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,21 @@ +import { UpdateAnonymousUserParams } from '..'; + +interface AnonSessionContext { + totalAnonSessionCount?: number; + lastAnonSession?: number; + firstAnonSession?: number; + webPushOptIn?: string; + lastPage?: string; + matchedCriteriaId: number; +} + +export interface TrackAnonSessionParams { + user: UpdateAnonymousUserParams; + createdAt: number; + deviceInfo: { + deviceId: string; + appPackageName: string; // customer-defined name + platform: string; + }; + anonSessionContext: AnonSessionContext; +} diff --git a/yarn.lock b/yarn.lock index 3124fe21..28d76dfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1711,32 +1711,11 @@ dependencies: "@types/node" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.56.6" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.6.tgz#d5dc16cac025d313ee101108ba5714ea10eb3ed0" - integrity sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*": +"@types/estree@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== - "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.17.43" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" @@ -1803,7 +1782,7 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -1923,7 +1902,7 @@ resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776" integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ== -"@types/uuid@^9.0.2": +"@types/uuid@^9.0.8": version "9.0.8" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== @@ -2069,125 +2048,125 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^1.2.0": @@ -2238,10 +2217,10 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-import-assertions@^1.7.6: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -2258,17 +2237,7 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.7.1: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== - -acorn@^8.8.2: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== - -acorn@^8.9.0: +acorn@^8.2.4, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -2505,11 +2474,11 @@ axios-mock-adapter@^1.22.0: is-buffer "^2.0.5" axios@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" - integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== + version "1.7.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" + integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -2722,36 +2691,26 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.14.5, browserslist@^4.23.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" - integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== +browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.22.2, browserslist@^4.23.0: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== dependencies: - caniuse-lite "^1.0.30001587" - electron-to-chromium "^1.4.668" - node-releases "^2.0.14" - update-browserslist-db "^1.0.13" - -browserslist@^4.21.9, browserslist@^4.22.2: - version "4.22.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" - integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== - dependencies: - caniuse-lite "^1.0.30001565" - electron-to-chromium "^1.4.601" - node-releases "^2.0.14" - update-browserslist-db "^1.0.13" + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" bs-logger@0.x: version "0.2.6" @@ -2843,15 +2802,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001565: - version "1.0.30001566" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz#61a8e17caf3752e3e426d4239c549ebbb37fef0d" - integrity sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA== - -caniuse-lite@^1.0.30001587: - version "1.0.30001612" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz#d34248b4ec1f117b70b24ad9ee04c90e0b8a14ae" - integrity sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g== +caniuse-lite@^1.0.30001646: + version "1.0.30001657" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001657.tgz#29fd504bffca719d1c6b63a1f6f840be1973a660" + integrity sha512-DPbJAlP8/BAXy3IgiWmZKItubb3TYGP0WscQQlVGIfT4s/YlFYVuJgyOsQNP7rJRChx/qdMeLJQJP0Sgg2yjNA== chalk@^2.4.2: version "2.4.2" @@ -3538,15 +3492,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.601: - version "1.4.607" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.607.tgz#340cc229b504966413716c6eae67d0f3d3702ff0" - integrity sha512-YUlnPwE6eYxzwBnFmawA8LiLRfm70R2aJRIUv0n03uHt/cUzzYACOogmvk8M2+hVzt/kB80KJXx7d5f5JofPvQ== - -electron-to-chromium@^1.4.668: - version "1.4.745" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz#9c202ce9cbf18a5b5e0ca47145fd127cc4dd2290" - integrity sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA== +electron-to-chromium@^1.5.4: + version "1.5.14" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.14.tgz#8de5fd941f4deede999f90503c4b5923fbe1962b" + integrity sha512-bEfPECb3fJ15eaDnu9LEJ2vPGD6W1vt7vZleSVyFhYuMIKm3vz/g9lt7IvEzgdwj58RjbPKUF2rXTCN/UW47tQ== emittery@^0.8.1: version "0.8.1" @@ -3575,10 +3524,10 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@^5.10.0, enhanced-resolve@^5.8.3: - version "5.16.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" - integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== +enhanced-resolve@^5.17.1, enhanced-resolve@^5.8.3: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -3667,10 +3616,10 @@ es-errors@^1.2.1, es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== es-object-atoms@^1.0.0: version "1.0.0" @@ -3709,6 +3658,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -4107,10 +4061,10 @@ filing-cabinet@^3.0.1: tsconfig-paths "^3.10.1" typescript "^3.9.7" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -4198,7 +4152,7 @@ flatten@^1.0.2: resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== -follow-redirects@^1.0.0, follow-redirects@^1.15.0: +follow-redirects@^1.0.0, follow-redirects@^1.15.6: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== @@ -4451,10 +4405,10 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== graphemer@^1.4.0: version "1.4.0" @@ -5965,12 +5919,7 @@ nanoclone@^0.2.1: resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -nanoid@^3.3.7: +nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== @@ -6010,10 +5959,10 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== node-source-walk@^4.0.0, node-source-walk@^4.2.0: version "4.2.0" @@ -6361,6 +6310,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -6785,9 +6739,9 @@ requirejs-config-file@^4.0.0: stringify-object "^3.2.1" requirejs@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" - integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== + version "2.3.7" + resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.7.tgz#0b22032e51a967900e0ae9f32762c23a87036bd0" + integrity sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw== requires-port@^1.0.0: version "1.0.0" @@ -6952,7 +6906,7 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -7490,7 +7444,7 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.1.3: +terser-webpack-plugin@^5.3.10: version "5.3.10" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== @@ -7812,13 +7766,13 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" - integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2: version "4.4.1" @@ -7903,10 +7857,10 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" -watchpack@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" - integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -8020,33 +7974,32 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.63.0: - version "5.76.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c" - integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: @@ -8164,14 +8117,14 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.6: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.13.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^3.0.0: version "3.0.0"