UNPKG

@causalfoundry/js-sdk

Version:

Causal Foundry WEB SDK (JS/TS)

332 lines (288 loc) 10.7 kB
import ECommmercePropertiesTI from '../ECommerce/typings-ti' import OxygenPropertiesTI from '../ECommerce/oxygen.typings-ti' import BloodPropertiesTI from '../ECommerce/blood.typings-ti' import MedicalEquipmentPropertiesTI from '../ECommerce/medicalEquipment.typings-ti' import NavigationPropertiesTI from './typings-ti' import { AppProperties, MediaType, ContentBlock, IdentifyProperties, MediaCatalog, MediaProperties, NavigationTypes, NudgeResponseProperties, PageProperties, RateProperties, ModuleType, SearchProperties, RateType, UserCatalog, CoreCatalogType, SiteCatalog, ModuleSelectionProperties } from './typings' import {injectEvent} from "../../core/injector" import {getCoreInstance} from '../../coreInstanceGetter' import {ICatalogRepository} from '../../core/repositories/catalog/CatalogRepository' import {ItemMinObject} from '../ECommerce/typings' import {hasRepeatedIds} from '../../utils' import CfExceptionHandler from "../../core/CfExceptionHandler"; let catalogRepository: ICatalogRepository; const moduleName = ContentBlock.Core /** * `title` may be overwritten by the developer * This variable stores the known title, that is, * the one provided by the CfAppStateMonitor or by developer */ let lastKnownTitle /** * @internal * * @param userId * @param properties * @param sendNow */ const logIdentifyEvent = (properties: IdentifyProperties, sendNow = false, ) => { injectEvent( properties, [NavigationPropertiesTI, ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI, MedicalEquipmentPropertiesTI], NavigationTypes.Identify, moduleName, '', sendNow) } /** * @internal * * @param eventType * @param properties * @param sendNow */ const logIngestEvent = ( eventType: NavigationTypes, properties: AppProperties | PageProperties | MediaProperties | SearchProperties | RateProperties| ModuleSelectionProperties, sendNow = false ) => { let checkProps: AppProperties | PageProperties | MediaProperties | SearchProperties | RateProperties | ModuleSelectionProperties | null = null; switch (eventType) { case NavigationTypes.App: checkProps = validateAppEvent(properties as AppProperties); break; case NavigationTypes.Page: checkProps = validatePageEvent(properties as PageProperties); break; case NavigationTypes.Media: checkProps = validateMediaEvent(properties as MediaProperties); break; case NavigationTypes.Search: checkProps = validateSearchEvent(properties as SearchProperties); break; case NavigationTypes.Rate: checkProps = validateRateEvent(properties as RateProperties); break; case NavigationTypes.ModuleSelection: checkProps = validateModuleSelectionEvent(properties as ModuleSelectionProperties); break; default: return; // Exit early if eventType is unrecognized } if (!checkProps) { return; // Exit if the properties validation failed } injectEvent( checkProps, [NavigationPropertiesTI, ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI, MedicalEquipmentPropertiesTI], eventType, moduleName, '', sendNow ); }; const logCatalogEvent = (coreCatalogType : CoreCatalogType, catalogProps : UserCatalog | MediaCatalog | SiteCatalog) => { switch (coreCatalogType) { case CoreCatalogType.User: catalogRepository.injectUser(catalogProps as UserCatalog) break; case CoreCatalogType.Site: catalogRepository.injectSite(catalogProps as SiteCatalog) break; case CoreCatalogType.Media: catalogRepository.injectMedia(catalogProps as MediaCatalog) break; case CoreCatalogType.Other: break; } } const validateModuleSelectionEvent = (properties: ModuleSelectionProperties) => { if (!properties.type) { new CfExceptionHandler().throwAndLogError(NavigationTypes.ModuleSelection, 'module type is required') return null } return properties } /** * Whenever the user interacts with the App, log that action with this method */ const validateAppEvent = (properties: AppProperties) : AppProperties => { if(properties.action == undefined){ new CfExceptionHandler().throwAndLogError(NavigationTypes.App, 'app action is required') return null } return properties } /** * SDK tracks automatically page changes by inspecting the URL. However, * depending on the app implementation, it may happen that the URL does not * change when a new view is presented to the user. For these cases, use this method */ const validatePageEvent = (properties: PageProperties): PageProperties => { if (properties.duration === 0) { // avoid noise due internal URLs that changes so fast return null } properties.title = lastKnownTitle ? lastKnownTitle : properties.title lastKnownTitle = undefined return properties } /** * Whenever the user interacts with a media element (Video, Audio, Image), log that * action with this method */ const validateMediaEvent = (properties: MediaProperties) : MediaProperties => { properties.time = properties.type === MediaType.Image ? 0 : properties.time if(properties.id == ''){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Media, 'empty media id') return null }else if(properties.type == undefined){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Media, 'media type is required') return null }else if(properties.action == undefined){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Media, 'media action is required') return null }else if(properties.time < 0){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Media, 'negative media seek time') return null } return properties } /** * This function logs into the platform a new search performed by the user, * including both, the parameters and the resulting ids */ const validateSearchEvent = (properties: SearchProperties): SearchProperties => { if(properties.query == ''){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Search, 'empty search query') return null }else if(properties.page < 0){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Search, 'negative search page') return null }else if(properties.module){ properties.module = properties.module ?? ModuleType.Core }else if (!properties.results_list) { properties.results_list = [] } if (properties.results_list.length !== 0) { properties.results_list.every(item => { if(item.id == ''){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Search, 'empty search result item id') return null }else if(item.type == undefined){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Search, 'empty search result item type') return null } }) properties.results_list = properties.results_list as ItemMinObject[] } if (hasRepeatedIds(properties.results_list)) { new CfExceptionHandler().throwAndLogError(NavigationTypes.Search, 'search result contains repeated ids') return null } return properties } /** * This function logs rate value provided by the user */ const validateRateEvent = (properties: RateProperties): RateProperties => { const RATE_MIN_VALUE = 0 const RATE_MAX_VALUE = 5 if(properties.rate_value < 0){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Rate, 'negative rate value') return null }else if (properties.rate_value < RATE_MIN_VALUE || properties.rate_value > RATE_MAX_VALUE) { new CfExceptionHandler().throwAndLogError(NavigationTypes.Rate, 'invalid rate range 0-5') return null }else if(properties.type == undefined){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Rate, 'rate type is required') return null }else if(properties.subject_id == undefined || properties.subject_id == ''){ new CfExceptionHandler().throwAndLogError(NavigationTypes.Rate, 'rate subject id is required') return null } return properties } /** * @internal * * @param properties * @param sendNow */ const logPushNotificationEvent = (properties: NudgeResponseProperties, sendNow = true): void => { injectEvent( properties, [NavigationPropertiesTI, ECommmercePropertiesTI, BloodPropertiesTI, OxygenPropertiesTI, MedicalEquipmentPropertiesTI], NavigationTypes.NudgeResponse, moduleName, '', sendNow) } /** * SDK has some autonomy to trigger events automatically. For instance, * it detects URL changes and notify causalfoundry.ai backend about that changes * and the duration. However, it is not able to find out which block that URL * belongs to. The goal of this method is to solve that. So, call it whenever * the user goes to another section within the site. For example, if the user * jumps from the e-commerce section to e-learning, call in this way: * * ``` * Navigation.setCurrentBlock(ContentBlock.Elearning) * ``` * * @param {ContentBlock} block the new section where the user has just jumped */ const setCurrentBlock = (block: ContentBlock) => { const core = getCoreInstance() core.setCurrentBlock(block) } /** * There are scenarious where the browser page title is not available, * for instance when the web is embedded within a webview. Use this function * whenever the new page is loaded to let the SDK know the title. * * Note that only is necessary when the standard `document.title` is not * available for any reason * * * @param title */ const setTitle = (title: string) => { // the timeout is for waiting for the page-changed event, if exists, // see CfSystem for more details setTimeout(() => { lastKnownTitle = title }, 15) } /** * Private function to inject CurrencyRepository * * @ignore */ const init = ( injectedCatalogRepository: ICatalogRepository ) => { catalogRepository = injectedCatalogRepository } export default { logIdentifyEvent, logIngestEvent, logCatalogEvent, logPushNotificationEvent, setCurrentBlock, setTitle, init }