Skip to content

Feat/catcher #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"preinstall": "npx only-allow yarn"
},
"devDependencies": {
"@types/stack-trace": "^0.0.33",
"eslint": "^9.14.0",
"eslint-config-codex": "^2.0.3",
"rimraf": "^6.0.1",
Expand All @@ -28,5 +29,9 @@
"typescript-eslint": "^8.13.0",
"vitest": "^2.1.4"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"dependencies": {
"@hawk.so/types": "^0.1.21",
"stack-trace": "^1.0.0-pre2"
}
}
7 changes: 4 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@
"test": "vitest run",
"coverage": "vitest run --coverage"
},
"dependencies": {
"mylib-utils": "workspace:^"
},
"devDependencies": {
"@vitest/coverage-v8": "^2.1.4",
"eslint": "^9.14.0",
"rimraf": "^6.0.1",
"typescript": "^5.6.3",
"vitest": "^2.1.4"
},
"dependencies": {
"react-native-exception-handler": "^2.10.10",
"stack-trace": "^0.0.10"
}
}
249 changes: 245 additions & 4 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,249 @@
import { myUtil } from 'mylib-utils';
import type { HawkEvent, HawkNodeJSInitialSettings } from '../types/index';

import {
EventContext,
AffectedUser,
EncodedIntegrationToken,
DecodedIntegrationToken,
EventData,
NodeJSAddons,
Json
} from '@hawk.so/types';
import { setJSExceptionHandler } from 'react-native-exception-handler';
import EventPayload from './modules/event';

class UnhandledRejection extends Error {}

let _instance: Catcher;

class Catcher {
private readonly type: string = 'errors/react-native';

private readonly token: EncodedIntegrationToken;

private readonly collectorEndpoint: string;

private readonly context?: EventContext;

private readonly beforeSend?: (event: EventData<NodeJSAddons>) => EventData<NodeJSAddons>;

constructor(settings: HawkNodeJSInitialSettings | string) {
if (typeof settings === 'string') {
settings = {
token: settings,
} as HawkNodeJSInitialSettings;
}

this.token = settings.token;
this.context = settings.context || undefined;
this.beforeSend = settings.beforeSend;

if (!this.token) {
throw new Error('Integration Token is missed. You can get it on https://hawk.so at Project Settings.');
}

try {
const integrationId = this.getIntegrationId();

this.collectorEndpoint = settings.collectorEndpoint || `https://${integrationId}.k1.hawk.so/`;

/**
* Set global handlers
*/
if (!settings.disableGlobalErrorsHandling) {
this.initGlobalHandlers();
}
} catch (error) {
console.log(error);
throw new Error('Invalid integration token');
}
}

/**
* Catcher package version
*/
private static getVersion(): string {
// TODO: get version from package.json
return '1.0.0';
}

/**
* Send test event from client
*/
public test(): void {
/**
* Create a dummy error event
* Error: Hawk NodeJS Catcher test message
*/
const fakeEvent = new Error('Hawk NodeJS Catcher test message');

/**
* Catch it and send to Hawk
*/
this.send(fakeEvent);
}

/**
* This method prepares and sends an Error to Hawk
* User can fire it manually on try-catch
*
* @param error - error to catch
* @param context — event context
* @param user - User identifier
*/
public send(error: Error, context?: EventContext, user?: AffectedUser): void {
/**
* Compose and send a request to Hawk
*/
this.formatAndSend(error, context, user);
}

/**
* Returns integration id from integration token
*/
private getIntegrationId(): string {
const decodedIntegrationTokenAsString = Buffer
.from(this.token, 'base64')
.toString('utf-8');
const decodedIntegrationToken: DecodedIntegrationToken = JSON.parse(decodedIntegrationTokenAsString);
const integrationId = decodedIntegrationToken.integrationId;

if (!integrationId || integrationId === '') {
throw new Error('Invalid integration token. There is no integration ID.');
}

return integrationId;
}

/**
* Define own error handlers
*/
private initGlobalHandlers(): void {
setJSExceptionHandler((error, isFatal) => {
console.log('Error', error, error.stack, isFatal);

this.sendErrorFormatted({
catcherType: this.type,
payload: {
title: error.name,
},
token: this.token,
})
}, true);
};

/**
* Format and send an error
*
* @param {Error} err - error to send
* @param {EventContext} context — event context
* @param {AffectedUser} user - User identifier
*/
private formatAndSend(err: Error, context?: EventContext, user?: AffectedUser): void {
const eventPayload = new EventPayload(err);
let payload: EventData<NodeJSAddons> = {
title: eventPayload.getTitle(),
type: eventPayload.getType(),
backtrace: eventPayload.getBacktrace(),
user: this.getUser(user),
context: this.getContext(context),
catcherVersion: Catcher.getVersion(),
};

/**
* Filter sensitive data
*/
if (typeof this.beforeSend === 'function') {
payload = this.beforeSend(payload);
}

this.sendErrorFormatted({
token: this.token,
catcherType: this.type,
payload,
});
}

/**
* Sends formatted EventData<NodeJSAddons> to the Collector
*
* @param {EventData<NodeJSAddons>>} eventFormatted - formatted event to send
*/
private async sendErrorFormatted(eventFormatted: HawkEvent): Promise<void | Response> {
return fetch(this.collectorEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(eventFormatted),
})
.catch((err: Error) => {
console.error(`[Hawk] Cannot send an event because of ${err.toString()}`);
});
}

/**
* Compose User object
*
* @param user - User identifier
*/
private getUser(user?: AffectedUser): AffectedUser|undefined {
return user;
}

/**
* Compose context object
*
* @param context - Any other information to send with event
*/
private getContext(context?: EventContext): Json {
const contextMerged = {};

if (this.context !== undefined) {
Object.assign(contextMerged, this.context);
}

if (context !== undefined) {
Object.assign(contextMerged, context);
}

return contextMerged;
}
}

/**
* Simple function that returns string
* Wrapper for Hawk NodeJS Catcher
*/
export default function main(): string {
return 'It works! ' + myUtil(1, 2);
export default class HawkCatcher {
/**
* Wrapper for HawkCatcher constructor
*
* @param settings - If settings is a string, it means an Integration Token
*/
public init(settings: HawkNodeJSInitialSettings | string): void {
_instance = new Catcher(settings);
}

/**
* Wrapper for HawkCatcher.send() method
*
* This method prepares and sends an Error to Hawk
* User can fire it manually on try-catch
*
* @param error - error to catch
* @param context — event context
* @param user - User identifier
*/
public static send(error: Error, context?: EventContext, user?: AffectedUser): void {
/**
* If instance is undefined then do nothing
*/
if (_instance) {
return _instance.send(error, context, user);
}
}
}

export {
HawkNodeJSInitialSettings
};
Loading
Loading