@causalfoundry/js-sdk
Version:
Causal Foundry WEB SDK (JS/TS)
359 lines (338 loc) • 9.21 kB
text/typescript
import {
AppProperties,
CoreCatalogType,
IdentifyProperties,
MediaCatalog,
MediaProperties,
MediaType,
ModuleSelectionProperties,
NavigationTypes,
PageProperties,
RateProperties,
SearchProperties,
SiteCatalog,
TrackProperties,
UserCatalog,
} from "./typings";
import { injectEvent } from "../../core/injector";
import CfExceptionHandler from "../../core/CfExceptionHandler";
import { NudgeResponseProperties } from "../../core/repositories/nudges/typings";
import { toSnakeCase } from "../../core/extFunctions";
import { GlobalSDKEventTypes } from "../../core/typings";
import {
validateAndSaveMediaCatalog,
validateAndSaveSiteCatalog,
validateAndSaveUserCatalog,
} from "./CoreCatalogValidator";
import { logSearchEvent } from "./legacyLoggerFunctions";
/**
* @internal
*
* @param userId
* @param properties
* @param sendNow
*/
const logIdentifyEvent = (
properties: IdentifyProperties,
sendNow = false,
eventTime?: string
) => {
injectEvent(
NavigationTypes.Identify,
properties.action,
properties,
sendNow,
"",
eventTime
);
};
/**
* @internal
*
* @param name
* @param property
* @param ctx
* @param sendNow
*/
const logIngestEvent = (
name: NavigationTypes | string,
ctx?:
| AppProperties
| PageProperties
| MediaProperties
| SearchProperties
| RateProperties
| ModuleSelectionProperties
| TrackProperties
| Record<string, string | number | boolean>,
sendNow = false,
eventTime?: string
) => {
let checkProps = true;
let property: string | undefined;
switch (name) {
case NavigationTypes.App:
checkProps = validateAppEvent(ctx as AppProperties);
if (checkProps) {
property = (ctx as AppProperties).action;
}
break;
case NavigationTypes.Page:
checkProps = validatePageEvent(ctx as PageProperties);
if (checkProps) {
property = (ctx as PageProperties).title;
}
break;
case NavigationTypes.Media:
checkProps = validateMediaEvent(ctx as MediaProperties);
if (checkProps) {
property = (ctx as MediaProperties).type;
}
break;
case NavigationTypes.Search: {
const searchContext = ctx as SearchProperties;
checkProps = validateSearchEvent(searchContext);
if (checkProps) {
property = searchContext.query;
if (Array.isArray(searchContext.results_list)) {
(ctx as SearchProperties).results_list = [
...new Set(searchContext.results_list),
];
} else {
(ctx as SearchProperties).results_list = [];
}
}
break;
}
case NavigationTypes.Rate:
checkProps = validateRateEvent(ctx as RateProperties);
if (checkProps) {
property = (ctx as RateProperties).type;
}
break;
case NavigationTypes.Track:
checkProps = validateTrackEvent(ctx as TrackProperties);
if (checkProps) {
name = toSnakeCase((ctx as TrackProperties).name);
property = (ctx as TrackProperties).property;
ctx = (ctx as TrackProperties).meta;
}
break;
case NavigationTypes.ModuleSelection:
checkProps = validateModuleSelectionEvent(
ctx as ModuleSelectionProperties
);
if (checkProps) {
property = (ctx as ModuleSelectionProperties).type;
}
break;
}
if (!checkProps) {
return; // Exit if the properties validation failed
}
if (
name === NavigationTypes.Media &&
(ctx as MediaProperties).type === MediaType.Image
) {
(ctx as MediaProperties).seek_time = 0;
}
injectEvent(name, property, ctx, sendNow, "", eventTime);
};
const logCatalogEvent = (
subjectId: string,
coreCatalogType: CoreCatalogType,
catalogProps: UserCatalog | MediaCatalog | SiteCatalog
) => {
switch (coreCatalogType) {
case CoreCatalogType.User:
validateAndSaveUserCatalog(subjectId, catalogProps as UserCatalog);
break;
case CoreCatalogType.Site:
validateAndSaveSiteCatalog(subjectId, catalogProps as SiteCatalog);
break;
case CoreCatalogType.Media:
validateAndSaveMediaCatalog(subjectId, catalogProps as MediaCatalog);
break;
case CoreCatalogType.Other:
break;
}
};
const validateModuleSelectionEvent = (
properties: ModuleSelectionProperties
): boolean => {
if (!properties.type) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.ModuleSelection,
"module type is required"
);
return false;
}
return true;
};
/**
* Whenever the user interacts with the App, log that action with this method
*/
const validateAppEvent = (properties: AppProperties): boolean => {
if (properties.action == undefined) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.App,
"app action is required"
);
return false;
}
return true;
};
/**
* 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): boolean => {
return properties.duration !== undefined && properties.duration >= 0;
};
/**
* Whenever the user interacts with a media element (Video, Audio, Image), log that
* action with this method
*/
const validateMediaEvent = (properties: MediaProperties): boolean => {
properties.seek_time =
properties.type === MediaType.Image ? 0 : properties.seek_time;
if (properties.id == "") {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Media,
"empty media id"
);
return false;
} else if (properties.type == undefined) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Media,
"media type is required"
);
return false;
} else if (properties.action == undefined) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Media,
"media action is required"
);
return false;
} else if (properties.seek_time && properties.seek_time < 0) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Media,
"negative media seek time"
);
return false;
}
return true;
};
/**
* 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): boolean => {
if (properties.page < 0) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Search,
"negative search page"
);
return false;
} else if (properties.results_list.length !== 0) {
properties.results_list.every((item) => {
if (item == "") {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Search,
"empty search result item id"
);
return false;
}
});
}
return true;
};
/**
* This function logs rate value provided by the user
*/
const validateRateEvent = (properties: RateProperties): boolean => {
const RATE_MIN_VALUE = 0;
const RATE_MAX_VALUE = 5;
if (properties.rate_value < 0) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Rate,
"negative rate value"
);
return false;
} 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 false;
} else if (properties.type == undefined) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Rate,
"rate type is required"
);
return false;
} else if (
properties.subject_id == undefined ||
properties.subject_id == ""
) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Rate,
"rate subject id is required"
);
return false;
}
return true;
};
const validateTrackEvent = (properties: TrackProperties): boolean => {
const snakeCaseName = toSnakeCase(properties.name);
// 1. Empty check
if (!properties.name || properties.name.trim() === "") {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Track,
"name for track event is required"
);
return false;
}
// 2. Collision check with GlobalSDKEventTypes
const reservedNames = new Set<string>(Object.values(GlobalSDKEventTypes));
if (reservedNames.has(snakeCaseName)) {
new CfExceptionHandler().throwAndLogError(
NavigationTypes.Track,
`The event name "${properties.name}" is reserved and cannot be used.`
);
return false;
}
return true;
};
/**
* @internal
*
* @param properties
* @param sendNow
*/
const logPushNotificationEvent = (
properties: NudgeResponseProperties,
sendNow = true,
eventTime?: string
): void => {
injectEvent(
NavigationTypes.ActionResponse,
properties.response,
properties,
sendNow,
"",
eventTime
);
};
export default {
logIdentifyEvent,
logIngestEvent,
logCatalogEvent,
logPushNotificationEvent,
// Legacy functions
logSearchEvent,
};