UNPKG

@causalfoundry/js-sdk

Version:

Causal Foundry WEB SDK (JS/TS)

302 lines (256 loc) 7.9 kB
import { NetworkProxy } from "../drivers/network/typings"; import { debug, Debug } from "../logging"; import { v4 as uuidv4 } from "uuid"; import isBrowser from "is-in-browser"; import { AppInfoObject, DeviceInfoObject, Event, EventMainObject, ExceptionObject, LogIngestorOptions, MainExceptionBody, QueueItem, } from "./typings"; import { getAppDetails, getDeviceDetails, getNetworkInformation, getOperatingSystem, } from "../drivers/CfSystem"; import { version } from "../../package.json"; import CfExceptionHandler from "./CfExceptionHandler"; export interface ISenderRepository { add(userId: string, deviceId: string, event: Event, forceSend: boolean): void; flush(); } export default class CfSender implements ISenderRepository { private flushing: boolean = false; private flushIntervalId: number = 0; private cfNetwork; private debug; private options; private endpointURL; private exceptionsEndPoint = "/ingest/sdk/crash"; private userId: string = ""; private deviceId: string = ""; private usingAsInternalWrapper: boolean = false; private queue: QueueItem[]; public constructor(cfNetwork: NetworkProxy, options: LogIngestorOptions) { this.queue = []; this.cfNetwork = cfNetwork; this.options = options; this.debug = debug(this.options.debug); // to avoid short timeout to save cpu usage if (this.options.flushInterval < 1000) { this.options.flushInterval = 1000; } if ( this.options.cacheEventsInLocalstorage && typeof Storage !== "undefined" ) { try { this.queue = JSON.parse( localStorage.getItem(this.options.cacheEventsKey) || "[]" ) as QueueItem[]; } catch (e) { this.debug( Debug.Error, `Error getting queue items from cache ${e.message || ""}` ); } } this.endpointURL = `${this.options.baseUrl}${this.options.ingestPath}`; this.updateQueueCache(); this.flushIntervalId = +setInterval(() => { this.flush(); }, this.options.flushInterval); } private removeQueueItemsExceedMaxRetries() { this.queue = this.queue.filter( (item: QueueItem) => item.retries <= this.options.flushMaxRetries ); this.updateQueueCache(); } private removeFromQueue(uuids: string[]) { this.queue = this.queue.filter( (item: QueueItem) => uuids.indexOf(item.uuid) === -1 ); this.updateQueueCache(); } private removeIndexFromQueue(indexToRemove: number) { this.queue.splice(indexToRemove, 1); this.updateQueueCache(); } private updateQueueCache() { if ( this.options.cacheEventsInLocalstorage && typeof Storage !== "undefined" ) { localStorage.setItem( this.options.cacheEventsKey, JSON.stringify(this.queue) ); } } private addRetries(uuids: string[]) { const time = +new Date(); this.queue .filter((item: QueueItem) => uuids.indexOf(item.uuid) !== -1) .forEach((item: QueueItem) => { item.retries = +item.retries + 1; item.lastRetry = time; }); this.updateQueueCache(); } public flush() { if(this.options.pauseSDK){ return } const flushItems = [...this.queue]; if (flushItems.length === 0) { this.debug(Debug.Log, "Skip sending empty list of logs "); return; } const uuids: string[] = flushItems.map((item: QueueItem) => item.uuid); const data = flushItems.map((item: QueueItem) => item.event); this.flushing = true; this.send(data, uuids); } public add( userId: string, deviceId: string, event: Event, forceSend: boolean = false, isUsingAsInternalWrapper: boolean = false ) { if(this.options.pauseSDK){ return } const item: QueueItem = { uuid: uuidv4(), event, time: +new Date(), retries: 0, }; this.userId = userId; this.deviceId = deviceId; this.usingAsInternalWrapper = isUsingAsInternalWrapper; const isDuplicate = this.queue.some(queueItem => queueItem.event === item.event); if (!isDuplicate) { this.queue.push(item); this.updateQueueCache(); // send immediately. In case of error, it was already enqueued // so will be tried again in interval tick if (forceSend) { this.send([item.event], [item.uuid]); } } } private send(dataList: Event[], uuids: string[]) { if(this.options.pauseSDK){ return } const { downlink, uplink } = getNetworkInformation(); // Remove duplicates const hashEvent = (event: Event) => JSON.stringify(event); // Convert event to string for comparison const getUniqueEvents = (data: Event[]): Event[] => { const seen = new Set<string>(); // Stores unique event hashes return data.filter(event => { const hash = hashEvent(event); // Create a unique identifier if (seen.has(hash)) return false; // If duplicate, skip it seen.add(hash); // Otherwise, add it to seen return true; }); }; const filteredData = getUniqueEvents(dataList); const deviceObject: DeviceInfoObject = { id: this.deviceId, ...getDeviceDetails(), }; const appObject: AppInfoObject = { id: isBrowser ? document?.location.host || "NO_URL_FOUND" : "NO_URL_FOUND", min_sdk_version: 0, target_sdk_version: 0, ...getAppDetails(), }; const eventMainObject: EventMainObject = { s_id: this.generateSessionID(this.userId), u_id: this.userId, app_info: appObject, d_info: deviceObject, sdk: `js/${version}`, up: this.checkDefaultNetworkValue(uplink), dn: this.checkDefaultNetworkValue(downlink), data: filteredData, }; return this.cfNetwork .send(this.endpointURL, eventMainObject) .then(() => { this.debug(Debug.Info, `Flushed ${filteredData.length} events`); this.removeFromQueue(uuids); }) .catch((error) => { if (error.response.data.data) { // Accessing specific properties from the error response this.removeIndexFromQueue(Number(error.response.data.data)) } this.addRetries(uuids); this.debug(Debug.Error, error.message || ""); }) .finally(() => { this.flushing = false; this.removeQueueItemsExceedMaxRetries(); }); } public sendException(exceptionList: ExceptionObject[]) { if(this.options.pauseSDK){ return } const deviceObject: DeviceInfoObject = { id: this.deviceId, ...getDeviceDetails(), }; const appObject: AppInfoObject = { id: isBrowser ? document?.location.host || "NO_URL_FOUND" : "NO_URL_FOUND", min_sdk_version: 0, target_sdk_version: 0, ...getAppDetails(), }; const mainExceptionBody: MainExceptionBody = { u_id: this.userId, d_info: deviceObject, app_info: appObject, sdk_version: `js/${version}`, data: exceptionList, }; return this.cfNetwork .send( `${this.options.baseUrl}${this.exceptionsEndPoint}`, mainExceptionBody ) .then(() => { this.debug(Debug.Info, `Flushed ${exceptionList.length} exceptions`); }) .catch((e) => { this.debug(Debug.Error, e.message || ""); }); } private generateSessionID(userId: string) { // return `${userId}_${startTimeInMillis}_${endTimeInMillis}`; return `${userId}_${Date.now()}`; } private checkDefaultNetworkValue(defaultValue: number): number { if (this.usingAsInternalWrapper) { return this.getRandomNumberValue(100, 1024); } return defaultValue; } private getRandomNumberValue(min: number, max: number): number { return Math.floor(Math.random() * (max - min) + min); } }