@causalfoundry/js-sdk
Version:
Causal Foundry WEB SDK (JS/TS)
332 lines (288 loc) • 10.7 kB
text/typescript
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
}