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