@causalfoundry/js-sdk
Version:
Causal Foundry WEB SDK (JS/TS)
364 lines (315 loc) • 11.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 {
Event,
ExceptionObject,
InternalInfoObject,
LogIngestorOptions,
MainExceptionBody,
QueueCatalogItem,
QueueItem,
} from "./typings";
import {getAppVersionDetails, getDeviceOSDetails, getNetworkInformation,} from "../drivers/CfSystem";
import {version} from "../../package.json";
import {offsetToISO8601} from "../utils";
import {CatalogItemModel} from "../modules/typings";
export interface ISenderRepository {
add(userId: string, deviceId: string, event: Event, forceSend: boolean): void;
flush();
}
export default class CfSender implements ISenderRepository {
private flushing = false;
private flushIntervalId = 0;
private cfNetwork;
private debug;
private options;
private endpointURL;
private catalogEndpointURL;
private exceptionsEndPoint = "/ingest/sdk/crash";
private userId = "";
private deviceId = "";
private usingAsInternalWrapper = false;
private queue: QueueItem[];
private catalogQueue: QueueCatalogItem[];
public constructor(cfNetwork: NetworkProxy, options: LogIngestorOptions) {
this.queue = [];
this.catalogQueue = [];
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[];
this.catalogQueue = JSON.parse(
localStorage.getItem(this.options.cacheCatalogKey) || "[]"
) as QueueCatalogItem[];
} catch (e) {
this.debug(
Debug.Error,
`Error getting queue items from cache ${e.message || ""}`
);
}
}
this.endpointURL = `${this.options.baseUrl}${this.options.ingestPath}`;
this.catalogEndpointURL = `${this.options.baseUrl}${this.options.dimensionPath}`;
this.updateQueueCache();
this.flushIntervalId = +setInterval(() => {
this.flush();
}, this.options.flushInterval);
}
public flush() {
if (this.options.pauseSDK) {
return;
}
const flushItems = [...this.queue];
const flushCatalogItems = [...this.catalogQueue];
if (flushItems.length === 0 || flushCatalogItems.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);
const catalogUUIDS: string[] = flushCatalogItems.map((item: QueueCatalogItem) => item.uuid);
const catalogData = flushCatalogItems.map((item: QueueCatalogItem) => item.catalog);
this.flushing = true;
this.send(data, uuids);
this.sendCatalog(catalogData, catalogUUIDS);
}
public add(
userId: string,
deviceId: string,
event: Event,
forceSend = false,
isUsingAsInternalWrapper = 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]);
}
}
}
public addCatalogItem(
catalog: CatalogItemModel,
isUsingAsInternalWrapper = false
) {
if (this.options.pauseSDK) {
return;
}
const item: QueueCatalogItem = {
uuid: uuidv4(),
catalog,
retries: 0,
};
this.usingAsInternalWrapper = isUsingAsInternalWrapper;
const isDuplicate = this.catalogQueue.some(
(queueCatalogItem) => queueCatalogItem.catalog === item.catalog
);
if (!isDuplicate) {
this.catalogQueue.push(item);
this.updateCatalogQueueCache();
}
}
private removeQueueItemsExceedMaxRetries() {
this.queue = this.queue.filter(
(item: QueueItem) => item.retries <= this.options.flushMaxRetries
);
this.updateQueueCache();
}
private removeQueueCatalogItemsExceedMaxRetries() {
this.catalogQueue = this.catalogQueue.filter(
(item: QueueCatalogItem) => item.retries <= this.options.flushMaxRetries
);
this.updateCatalogQueueCache();
}
private removeFromQueue(uuids: string[]) {
this.queue = this.queue.filter(
(item: QueueItem) => uuids.indexOf(item.uuid) === -1
);
this.updateQueueCache();
}
private removeFromCatalogQueue(uuids: string[]) {
this.catalogQueue = this.catalogQueue.filter(
(item: QueueCatalogItem) => uuids.indexOf(item.uuid) === -1
);
this.updateCatalogQueueCache();
}
private removeIndexFromQueue(indexToRemove: number) {
this.queue.splice(indexToRemove, 1);
this.updateQueueCache();
}
private removeIndexFromCatalogQueue(indexToRemove: number) {
this.catalogQueue.splice(indexToRemove, 1);
this.updateCatalogQueueCache();
}
private updateQueueCache() {
if (
this.options.cacheEventsInLocalstorage &&
typeof Storage !== "undefined"
) {
localStorage.setItem(
this.options.cacheEventsKey,
JSON.stringify(this.queue)
);
}
}
private updateCatalogQueueCache() {
if (
this.options.cacheEventsInLocalstorage &&
typeof Storage !== "undefined"
) {
localStorage.setItem(
this.options.cacheCatalogKey,
JSON.stringify(this.catalogQueue)
);
}
}
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();
}
private addCatalogRetries(uuids: string[]) {
const time = +new Date();
this.catalogQueue
.filter((item: QueueCatalogItem) => uuids.indexOf(item.uuid) !== -1)
.forEach((item: QueueCatalogItem) => {
item.retries = +item.retries + 1;
item.lastRetry = time;
});
this.updateCatalogQueueCache();
}
private send(dataList: Event[], uuids: string[]) {
if (this.options.pauseSDK) {
return;
}
// 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 internalInfoObject: InternalInfoObject = {
s_id: this.generateSessionID(this.userId),
sdk: `js/${version}`,
app_id: isBrowser
? document?.location.host || "NO_URL_FOUND"
: "NO_URL_FOUND",
app_version: getAppVersionDetails(),
device_id: this.deviceId,
device_os: getDeviceOSDetails(),
};
return this.cfNetwork
.send(this.endpointURL, {
data: filteredData,
internal: internalInfoObject,
id: this.userId,
})
.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();
});
}
private sendCatalog(dataList: CatalogItemModel[], uuids: string[]) {
if (this.options.pauseSDK) {
return;
}
// Remove duplicates
const hashEvent = (event: CatalogItemModel) => JSON.stringify(event); // Convert event to string for comparison
const getUniqueEvents = (data: CatalogItemModel[]): CatalogItemModel[] => {
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);
return this.cfNetwork
.send(this.catalogEndpointURL, {
data: filteredData
})
.then(() => {
this.debug(Debug.Info, `Flushed ${filteredData.length} events`);
this.removeFromCatalogQueue(uuids);
})
.catch((error) => {
if (error.response.data.data) {
// Accessing specific properties from the error response
this.removeIndexFromCatalogQueue(Number(error.response.data.data));
}
this.addCatalogRetries(uuids);
this.debug(Debug.Error, error.message || "");
})
.finally(() => {
this.flushing = false;
this.removeQueueCatalogItemsExceedMaxRetries();
});
}
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);
}
}