fyipe-staging
Version:
Fyipe is a JS package that tracks error event and send logs from your applications to your fyipe dashboard.
303 lines (291 loc) • 10.2 kB
JavaScript
import FyipeListener from './listener';
import Util from './util';
import { v4 as uuidv4 } from 'uuid';
import axios from 'axios';
import { name, version } from '../package.json';
class ErrorTracker {
// constructor to set up global listeners
constructor(apiUrl, errorTrackerId, errorTrackerKey, options = {}) {
this._setErrorTrackerId(errorTrackerId);
this._setApiUrl(apiUrl);
this._setErrorTrackerKey(errorTrackerKey);
this.tags = [];
this.extras = [];
this.isWindow = false;
this.fingerprint = [];
this.options = {
maxTimeline: 5,
captureCodeSnippet: true,
};
this.MAX_ITEMS_ALLOWED_IN_STACK = 100;
this.configKeys = ['baseUrl'];
// set up option
this._setUpOptions(options);
this._setEventId();
this.isWindow = typeof window !== 'undefined';
this.listenerObj = new FyipeListener(
this.getEventId(),
this.isWindow,
this.options
); // Initialize Listener for timeline
this.utilObj = new Util(this.options);
// set up error listener
if (this.isWindow) {
this._setUpErrorListener();
} else {
this._setUpNodeErrorListener();
}
}
_setErrorTrackerId(errorTrackerId) {
this.errorTrackerId = errorTrackerId;
}
_setErrorTrackerKey(errorTrackerKey) {
this.errorTrackerKey = errorTrackerKey;
}
_setApiUrl(apiUrl) {
this.apiUrl = `${apiUrl}/error-tracker/${this.errorTrackerId}/track`;
}
_setUpOptions(options) {
for (const [key, value] of Object.entries(options)) {
// proceed with current key if it is not in the config keys
if (!this.configKeys.includes(key)) {
// if key is in allowed options keys
if (this.options[key]) {
// set max timeline properly after checking conditions
if (
key === 'maxTimeline' &&
(value > this.MAX_ITEMS_ALLOWED_IN_STACK || value < 1)
) {
this.options[key] = this.MAX_ITEMS_ALLOWED_IN_STACK;
} else if (key === 'captureCodeSnippet') {
const isBoolean = typeof value === 'boolean'; // check if the passed value is a boolean
// set boolean value if boolean or set default `true` if annything other than boolean is passed
this.options[key] = isBoolean ? value : true;
} else {
this.options[key] = value;
}
}
}
}
}
_setEventId() {
this.eventId = uuidv4();
}
getEventId() {
return this.eventId;
}
setTag(key, value) {
if (!(typeof key === 'string') || !(typeof value === 'string')) {
return 'Invalid Tags type';
}
// get the index if the key exist already
const index = this.tags.findIndex(tag => tag.key === key);
if (index !== -1) {
// replace value if it exist
this.tags[index].value = value;
} else {
// push key and value if it doesnt
this.tags = [...this.tags, { key, value }];
}
}
// pass an array of tags
setTags(tags) {
if (!Array.isArray(tags)) {
return 'Invalid Tags type';
}
tags.forEach(element => {
if (element.key && element.value) {
this.setTag(element.key, element.value);
}
});
}
_getTags() {
return this.tags;
}
setExtras(extras) {
extras.forEach(element => {
if (element.key && element.extra) {
this.setExtra(element.key, element.extra);
}
});
}
setExtra(key, extra) {
this.extras = { ...this.extras, [key]: extra };
}
setFingerprint(keys) {
if (!(typeof keys === 'string') && !Array.isArray(keys)) {
return 'Invalid Fingerprint Format';
}
this.fingerprint = keys ? (Array.isArray(keys) ? keys : [keys]) : [];
}
_getFingerprint(errorMessage) {
// if no fingerprint exist currently
if (this.fingerprint.length < 1) {
// set up finger print based on error since none exist
this.setFingerprint(errorMessage);
}
return this.fingerprint;
}
// set up error listener
_setUpErrorListener() {
const _this = this;
window.onerror = async function(message, file, line, col, error) {
const errorEvent = { message, file, line, col, error };
const string = errorEvent.message
? errorEvent.message.toLowerCase()
: errorEvent.toLowerCase();
const substring = 'script error';
if (string.indexOf(substring) > -1) {
return; // third party error
} else {
// construct the error object
const errorObj = await _this.utilObj._getErrorStackTrace(
errorEvent
);
// set the a handled tag
_this.setTag('handled', 'false');
// prepare to send to server
_this.prepareErrorObject('error', errorObj);
// send to the server
_this.sendErrorEventToServer();
}
};
}
_setUpNodeErrorListener() {
const _this = this;
process
.on('uncaughtException', err => {
// display for the user
// eslint-disable-next-line no-console
console.log(`${err}`);
// any uncaught error
_this._manageErrorNode(err);
})
.on('unhandledRejection', err => {
// display this for the user
// eslint-disable-next-line no-console
console.log(`UnhandledPromiseRejectionWarning: ${err.stack}`);
// any unhandled promise error
_this._manageErrorNode(err);
});
}
async _manageErrorNode(error) {
// construct the error object
const errorObj = await this.utilObj._getErrorStackTrace(error);
// set the a handled tag
this.setTag('handled', 'false');
// prepare to send to server
this.prepareErrorObject('error', errorObj);
// send to the server
return this.sendErrorEventToServer();
}
addToTimeline(category, content, type) {
const timeline = {
category,
data: {
content,
},
type,
};
this.listenerObj.logCustomTimelineEvent(timeline);
}
getTimeline() {
return this.listenerObj.getTimeline();
}
captureMessage(message) {
// set the a handled tag
this.setTag('handled', 'true');
this.prepareErrorObject('message', { message });
// send to the server
return this.sendErrorEventToServer();
}
async captureException(error) {
// construct the error object
const errorObj = await this.utilObj._getErrorStackTrace(error);
// set the a handled tag
this.setTag('handled', 'true');
this.prepareErrorObject('exception', errorObj);
// send to the server
return this.sendErrorEventToServer();
}
_setHost() {
if (this.isWindow) {
// Web apps
this.setTag('url', window.location.origin);
} else {
// JS Backend
// TODO create a way to get host on the backend
}
}
prepareErrorObject(type, errorStackTrace) {
// log event
const content = {
message: errorStackTrace.message,
};
this.listenerObj.logErrorEvent(content, type);
// set the host as a tag to be used later
this._setHost();
// get current timeline
const timeline = this.getTimeline();
// get device location and details
const deviceDetails = this.utilObj._getUserDeviceDetails();
const tags = this._getTags();
const fingerprint = this._getFingerprint(errorStackTrace.message); // default fingerprint will be the message from the error stacktrace
// get event ID
// Temporary display the state of the error stack, timeline and device details when an error occur
// prepare the event so it can be sent to the server
this.event = {
type,
timeline,
exception: errorStackTrace,
deviceDetails,
eventId: this.getEventId(),
tags,
fingerprint,
errorTrackerKey: this.errorTrackerKey,
sdk: this.getSDKDetails(),
};
}
async sendErrorEventToServer() {
let content;
await this._makeApiRequest(this.event)
.then(response => {
content = response;
// generate a new event Id
this._setEventId();
// clear the timeline after a successful call to the server
this._clear(this.getEventId());
})
.catch(error => (content = error));
return content;
}
_makeApiRequest(data) {
return new Promise((resolve, reject) => {
axios
.post(this.apiUrl, data)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
});
});
}
getCurrentEvent() {
return this.event;
}
getSDKDetails() {
return { name, version };
}
_clear(newEventId) {
// clear tags
this.tags = [];
// clear extras
this.extras = [];
// clear fingerprint
this.fingerprint = [];
// clear timeline
this.listenerObj.clearTimeline(newEventId);
}
}
export default ErrorTracker;