Skip to content
Open
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
File renamed without changes.
8 changes: 8 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@ export type { HawkStorage } from './storages/hawk-storage';
export { HawkUserManager } from './users/hawk-user-manager';
export type { Logger, LogType } from './logger/logger';
export { isLoggerSet, setLogger, resetLogger, log } from './logger/logger';
export { Sanitizer } from './modules/sanitizer';
export type { SanitizerTypeHandler } from './modules/sanitizer';
export { StackParser } from './modules/stack-parser';
export { buildElementSelector } from './utils/selector';
export type { Transport } from './transports/transport';
export { EventRejectedError } from './errors';
export { isErrorProcessed, markErrorAsProcessed } from './utils/event';
export { isPlainObject, validateUser, validateContext, isValidEventPayload, isValidBreadcrumb } from './utils/validation';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { log } from '@hawk.so/core';
import { log } from '../logger/logger';

/**
* Sends AJAX request and wait for some time.
Expand Down
302 changes: 302 additions & 0 deletions packages/core/src/modules/sanitizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { isPlainObject } from '../utils/validation';

/**
* Custom type handler for Sanitizer.
*
* Allows user to register their own formatters from external packages.
*/
export interface SanitizerTypeHandler {
/**
* Checks if this handler should be applied to given value
*
* @returns `true`
*/
check: (target: any) => boolean;

/**
* Formats the value into a sanitized representation
*/
format: (target: any) => any;
}

/**
* This class provides methods for preparing data to sending to Hawk
* - trim long strings
* - represent big objects as "<big object>"
* - represent class as <class SomeClass> or <instance of SomeClass>
*/
export class Sanitizer {
/**
* Maximum string length
*/
private static readonly maxStringLen: number = 200;

/**
* If object in stringified JSON has more keys than this value,
* it will be represented as "<big object>"
*/
private static readonly maxObjectKeysCount: number = 20;

/**
* Maximum depth of context object
*/
private static readonly maxDepth: number = 5;

/**
* Maximum length of context arrays
*/
private static readonly maxArrayLength: number = 10;

/**
* Custom type handlers registered via {@link registerHandler}.

Check warning on line 52 in packages/core/src/modules/sanitizer.ts

View workflow job for this annotation

GitHub Actions / lint

The type 'sanitize' is undefined

Check warning on line 52 in packages/core/src/modules/sanitizer.ts

View workflow job for this annotation

GitHub Actions / lint

The type 'registerHandler' is undefined
*
* Checked in {@link sanitize} before built-in type checks.
*/
private static readonly customHandlers: SanitizerTypeHandler[] = [];

/**
* Check if passed variable is an object
*
* @param target - variable to check
*/
public static isObject(target: any): boolean {
return isPlainObject(target);
}

/**
* Register a custom type handler.
* Handlers are checked before built-in type checks, in reverse registration order
* (last registered = highest priority).
*
* @param handler - handler to register
*/
public static registerHandler(handler: SanitizerTypeHandler): void {
Sanitizer.customHandlers.unshift(handler);
}

/**
* Apply sanitizing for array/object/primitives
*
* @param data - any object to sanitize
* @param depth - current depth of recursion
* @param seen - Set of already seen objects to prevent circular references
*/
public static sanitize(data: any, depth = 0, seen = new WeakSet<object>()): any {
/**
* Check for circular references on objects and arrays
*/
if (data !== null && typeof data === 'object') {
if (seen.has(data)) {
return '<circular>';
}
seen.add(data);
}

/**
* If value is an Array, apply sanitizing for each element
*/
if (Sanitizer.isArray(data)) {
return this.sanitizeArray(data, depth + 1, seen);
}

// Check additional handlers provided by env-specific modules or users
// to sanitize some additional cases (e.g. specific object types)
for (const handler of Sanitizer.customHandlers) {
if (handler.check(data)) {
return handler.format(data);
}
}

/**
* If values is a not-constructed class, it will be formatted as "<class SomeClass>"
* class Editor {...} -> <class Editor>
*/
if (Sanitizer.isClassPrototype(data)) {
return Sanitizer.formatClassPrototype(data);

/**
* If values is a some class instance, it will be formatted as "<instance of SomeClass>"
* new Editor() -> <instance of Editor>
*/
} else if (Sanitizer.isClassInstance(data)) {
return Sanitizer.formatClassInstance(data);

/**
* If values is an object, do recursive call
*/
} else if (Sanitizer.isObject(data)) {
return Sanitizer.sanitizeObject(data, depth + 1, seen);

/**
* If values is a string, trim it for max-length
*/
} else if (Sanitizer.isString(data)) {
return Sanitizer.trimString(data);
}

/**
* If values is a number, boolean and other primitive, leave as is
*/
return data;
}

/**
* Apply sanitizing for each element of the array
*
* @param arr - array to sanitize
* @param depth - current depth of recursion
* @param seen - Set of already seen objects to prevent circular references
*/
private static sanitizeArray(arr: any[], depth: number, seen: WeakSet<object>): any[] {
/**
* If the maximum length is reached, slice array to max length and add a placeholder
*/
const length = arr.length;

if (length > Sanitizer.maxArrayLength) {
arr = arr.slice(0, Sanitizer.maxArrayLength);
arr.push(`<${length - Sanitizer.maxArrayLength} more items...>`);
}

return arr.map((item: any) => {
return Sanitizer.sanitize(item, depth, seen);
});
}

/**
* Process object values recursive
*
* @param data - object to beautify
* @param depth - current depth of recursion
* @param seen - Set of already seen objects to prevent circular references
*/
private static sanitizeObject(data: {
[key: string]: any
}, depth: number, seen: WeakSet<object>): Record<string, any> | '<deep object>' | '<big object>' {
/**
* If the maximum depth is reached, return a placeholder
*/
if (depth > Sanitizer.maxDepth) {
return '<deep object>';
}

/**
* If the object has more keys than the limit, return a placeholder
*/
if (Object.keys(data).length > Sanitizer.maxObjectKeysCount) {
return '<big object>';
}

const result: any = {};

for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
result[key] = Sanitizer.sanitize(data[key], depth, seen);
}
}

return result;
}

/**
* Check if passed variable is an array
*
* @param target - variable to check
*/
private static isArray(target: any): boolean {
return Array.isArray(target);
}

/**
* Check if passed variable is a not-constructed class
*
* @param target - variable to check
*/
private static isClassPrototype(target: any): boolean {
if (!target || !target.constructor) {
return false;
}

/**
* like
* "function Function {
* [native code]
* }"
*/
const constructorStr = target.constructor.toString();

return constructorStr.includes('[native code]') && constructorStr.includes('Function');
}

/**
* Check if passed variable is a constructed class instance
*
* @param target - variable to check
*/
private static isClassInstance(target: any): boolean {
return target && target.constructor && (/^class \S+ {/).test(target.constructor.toString());
}

/**
* Check if passed variable is a string
*
* @param target - variable to check
*/
private static isString(target: any): boolean {
return typeof target === 'string';
}

/**
* Return name of a passed class
*
* @param target - not-constructed class
*/
private static getClassNameByPrototype(target: any): string {
return target.name;
}

/**
* Return name of a class by an instance
*
* @param target - instance of some class
*/
private static getClassNameByInstance(target: any): string {
return Sanitizer.getClassNameByPrototype(target.constructor);
}

/**
* Trim string if it reaches max length
*
* @param target - string to check
*/
private static trimString(target: string): string {
if (target.length > Sanitizer.maxStringLen) {
return target.substring(0, Sanitizer.maxStringLen) + '…';
}

return target;
}

/**
* Represent not-constructed class as "<class SomeClass>"
*
* @param target - class to format
*/
private static formatClassPrototype(target: any): string {
const className = Sanitizer.getClassNameByPrototype(target);

return `<class ${className}>`;
}

/**
* Represent a some class instance as a "<instance of SomeClass>"
*
* @param target - class instance to format
*/
private static formatClassInstance(target: any): string {
const className = Sanitizer.getClassNameByInstance(target);

return `<instance of ${className}>`;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { StackFrame } from 'error-stack-parser';
import ErrorStackParser from 'error-stack-parser';
import type { BacktraceFrame, SourceCodeLine } from '@hawk.so/types';
import fetchTimer from './fetchTimer';
import fetchTimer from './fetch-timer';

/**
* This module prepares parsed backtrace
*/
export default class StackParser {
export class StackParser {
/**
* Prevents loading one file several times
* name -> content
Expand Down Expand Up @@ -48,7 +48,7 @@ export default class StackParser {
try {
if (!frame.fileName) {
return null;
};
}

if (!this.isValidUrl(frame.fileName)) {
return null;
Expand Down Expand Up @@ -118,9 +118,9 @@ export default class StackParser {
/**
* Downloads source file
*
* @param {string} fileName - name of file to download
* @param fileName - name of file to download
*/
private async loadSourceFile(fileName): Promise<string | null> {
private async loadSourceFile(fileName: string): Promise<string | null> {
if (this.sourceFilesCache[fileName] !== undefined) {
return this.sourceFilesCache[fileName];
}
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/transports/transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { CatcherMessage } from '@hawk.so/types';
import { CatcherMessageType } from '@hawk.so/types';

Check failure on line 2 in packages/core/src/transports/transport.ts

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`

/**
* Transport interface — anything that can send a CatcherMessage
*/
export interface Transport<T extends CatcherMessageType> {
send(message: CatcherMessage<T>): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { log } from '@hawk.so/core';
import { log } from '../logger/logger';

/**
* Symbol to mark error as processed by Hawk
*/
const errorSentShadowProperty = Symbol('__hawk_processed__');

/**
* Check if the error has alrady been sent to Hawk.
* Check if the error has already been sent to Hawk.
*
* Motivation:
* Some integrations may catch errors on their own side and then normally re-throw them down.
Expand All @@ -20,7 +20,7 @@ export function isErrorProcessed(error: unknown): boolean {
return false;
}

return error[errorSentShadowProperty] === true;
return (error as Record<symbol, unknown>)[errorSentShadowProperty] === true;
}

/**
Expand All @@ -35,7 +35,7 @@ export function markErrorAsProcessed(error: unknown): void {
}

Object.defineProperty(error, errorSentShadowProperty, {
enumerable: false, // Prevent from beight collected by Hawk
enumerable: false, // Prevent from being collected by Hawk
value: true,
writable: true,
configurable: true,
Expand Down
Loading
Loading