Skip to content

Commit 669e280

Browse files
committed
refactor(javascript): add core package
1 parent 5220d8c commit 669e280

File tree

9 files changed

+560
-0
lines changed

9 files changed

+560
-0
lines changed

packages/core/package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@hawk.so/core",
3+
"version": "1.0.0",
4+
"description": "Core utilities for Hawk.so error tracking SDKs",
5+
"files": [
6+
"dist"
7+
],
8+
"main": "./dist/hawk-core.umd.js",
9+
"module": "./dist/hawk-core.mjs",
10+
"types": "dist/index.d.ts",
11+
"exports": {
12+
".": {
13+
"types": "./dist/index.d.ts",
14+
"import": "./dist/hawk-core.mjs",
15+
"require": "./dist/hawk-core.umd.js"
16+
}
17+
},
18+
"scripts": {
19+
"build": "vite build"
20+
},
21+
"repository": {
22+
"type": "git",
23+
"url": "git+https://github.com/codex-team/hawk.javascript.git",
24+
"directory": "packages/core"
25+
},
26+
"author": {
27+
"name": "CodeX",
28+
"email": "team@codex.so"
29+
},
30+
"license": "AGPL-3.0-only",
31+
"bugs": {
32+
"url": "https://github.com/codex-team/hawk.javascript/issues"
33+
},
34+
"homepage": "https://github.com/codex-team/hawk.javascript#readme",
35+
"devDependencies": {
36+
"@hawk.so/types": "0.5.2",
37+
"vite": "^7.3.1",
38+
"vite-plugin-dts": "^4.2.4"
39+
}
40+
}

packages/core/src/errors.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Error triggered when event was rejected by beforeSend method
3+
*/
4+
export class EventRejectedError extends Error {
5+
/**
6+
* @param message - error message
7+
*/
8+
constructor(message: string) {
9+
super(message);
10+
this.name = 'EventRejectedError';
11+
}
12+
}

packages/core/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @hawk.so/core
3+
*
4+
* Core utilities for Hawk.so error tracking SDKs
5+
* Environment-agnostic code that can be used in browser and server environments
6+
*/
7+
8+
export { EventRejectedError } from './errors';
9+
export { default as Sanitizer } from './modules/sanitizer';
10+
export { isErrorProcessed, markErrorAsProcessed } from './utils/event';
11+
export { validateUser, validateContext } from './utils/validation';
12+
export { default as log } from './utils/log';
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/**
3+
* This class provides methods for preparing data to sending to Hawk
4+
* - trim long strings
5+
* - represent html elements like <div ...> as "<div>" instead of "{}"
6+
* - represent big objects as "<big object>"
7+
* - represent class as <class SomeClass> or <instance of SomeClass>
8+
*/
9+
export default class Sanitizer {
10+
/**
11+
* Maximum string length
12+
*/
13+
private static readonly maxStringLen: number = 200;
14+
15+
/**
16+
* If object in stringified JSON has more keys than this value,
17+
* it will be represented as "<big object>"
18+
*/
19+
private static readonly maxObjectKeysCount: number = 20;
20+
21+
/**
22+
* Maximum depth of context object
23+
*/
24+
private static readonly maxDepth: number = 5;
25+
26+
/**
27+
* Maximum length of context arrays
28+
*/
29+
private static readonly maxArrayLength: number = 10;
30+
31+
/**
32+
* Check if passed variable is an object
33+
*
34+
* @param target - variable to check
35+
*/
36+
public static isObject(target: any): boolean {
37+
return Sanitizer.typeOf(target) === 'object';
38+
}
39+
40+
/**
41+
* Apply sanitizing for array/object/primitives
42+
*
43+
* @param data - any object to sanitize
44+
* @param depth - current depth of recursion
45+
* @param seen - Set of already seen objects to prevent circular references
46+
*/
47+
public static sanitize(data: any, depth = 0, seen = new WeakSet<object>()): any {
48+
/**
49+
* Check for circular references on objects and arrays
50+
*/
51+
if (data !== null && typeof data === 'object') {
52+
if (seen.has(data)) {
53+
return '<circular>';
54+
}
55+
seen.add(data);
56+
}
57+
58+
/**
59+
* If value is an Array, apply sanitizing for each element
60+
*/
61+
if (Sanitizer.isArray(data)) {
62+
return this.sanitizeArray(data, depth + 1, seen);
63+
64+
/**
65+
* If value is an Element, format it as string with outer HTML
66+
* HTMLDivElement -> "<div ...></div>"
67+
*/
68+
} else if (Sanitizer.isElement(data)) {
69+
return Sanitizer.formatElement(data);
70+
71+
/**
72+
* If values is a not-constructed class, it will be formatted as "<class SomeClass>"
73+
* class Editor {...} -> <class Editor>
74+
*/
75+
} else if (Sanitizer.isClassPrototype(data)) {
76+
return Sanitizer.formatClassPrototype(data);
77+
78+
/**
79+
* If values is a some class instance, it will be formatted as "<instance of SomeClass>"
80+
* new Editor() -> <instance of Editor>
81+
*/
82+
} else if (Sanitizer.isClassInstance(data)) {
83+
return Sanitizer.formatClassInstance(data);
84+
85+
/**
86+
* If values is an object, do recursive call
87+
*/
88+
} else if (Sanitizer.isObject(data)) {
89+
return Sanitizer.sanitizeObject(data, depth + 1, seen);
90+
91+
/**
92+
* If values is a string, trim it for max-length
93+
*/
94+
} else if (Sanitizer.isString(data)) {
95+
return Sanitizer.trimString(data);
96+
}
97+
98+
/**
99+
* If values is a number, boolean and other primitive, leave as is
100+
*/
101+
return data;
102+
}
103+
104+
/**
105+
* Apply sanitizing for each element of the array
106+
*
107+
* @param arr - array to sanitize
108+
* @param depth - current depth of recursion
109+
* @param seen - Set of already seen objects to prevent circular references
110+
*/
111+
private static sanitizeArray(arr: any[], depth: number, seen: WeakSet<object>): any[] {
112+
/**
113+
* If the maximum length is reached, slice array to max length and add a placeholder
114+
*/
115+
const length = arr.length;
116+
117+
if (length > Sanitizer.maxArrayLength) {
118+
arr = arr.slice(0, Sanitizer.maxArrayLength);
119+
arr.push(`<${length - Sanitizer.maxArrayLength} more items...>`);
120+
}
121+
122+
return arr.map((item: any) => {
123+
return Sanitizer.sanitize(item, depth, seen);
124+
});
125+
}
126+
127+
/**
128+
* Process object values recursive
129+
*
130+
* @param data - object to beautify
131+
* @param depth - current depth of recursion
132+
* @param seen - Set of already seen objects to prevent circular references
133+
*/
134+
private static sanitizeObject(data: { [key: string]: any }, depth: number, seen: WeakSet<object>): Record<string, any> | '<deep object>' | '<big object>' {
135+
/**
136+
* If the maximum depth is reached, return a placeholder
137+
*/
138+
if (depth > Sanitizer.maxDepth) {
139+
return '<deep object>';
140+
}
141+
142+
/**
143+
* If the object has more keys than the limit, return a placeholder
144+
*/
145+
if (Object.keys(data).length > Sanitizer.maxObjectKeysCount) {
146+
return '<big object>';
147+
}
148+
149+
const result: any = {};
150+
151+
for (const key in data) {
152+
if (Object.prototype.hasOwnProperty.call(data, key)) {
153+
result[key] = Sanitizer.sanitize(data[key], depth, seen);
154+
}
155+
}
156+
157+
return result;
158+
}
159+
160+
/**
161+
* Check if passed variable is an array
162+
*
163+
* @param target - variable to check
164+
*/
165+
private static isArray(target: any): boolean {
166+
return Array.isArray(target);
167+
}
168+
169+
/**
170+
* Check if passed variable is a not-constructed class
171+
*
172+
* @param target - variable to check
173+
*/
174+
private static isClassPrototype(target: any): boolean {
175+
if (!target || !target.constructor) {
176+
return false;
177+
}
178+
179+
/**
180+
* like
181+
* "function Function {
182+
* [native code]
183+
* }"
184+
*/
185+
const constructorStr = target.constructor.toString();
186+
187+
return constructorStr.includes('[native code]') && constructorStr.includes('Function');
188+
}
189+
190+
/**
191+
* Check if passed variable is a constructed class instance
192+
*
193+
* @param target - variable to check
194+
*/
195+
private static isClassInstance(target: any): boolean {
196+
return target && target.constructor && (/^class \S+ {/).test(target.constructor.toString());
197+
}
198+
199+
/**
200+
* Check if passed variable is a string
201+
*
202+
* @param target - variable to check
203+
*/
204+
private static isString(target: any): boolean {
205+
return typeof target === 'string';
206+
}
207+
208+
/**
209+
* Return string representation of the object type
210+
*
211+
* @param object - object to get type
212+
*/
213+
private static typeOf(object: any): string {
214+
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
215+
}
216+
217+
/**
218+
* Check if passed variable is an HTML Element (environment-agnostic)
219+
*
220+
* @param target - variable to check
221+
*/
222+
private static isElement(target: any): boolean {
223+
return typeof Element !== 'undefined' && target instanceof Element;
224+
}
225+
226+
/**
227+
* Return name of a passed class
228+
*
229+
* @param target - not-constructed class
230+
*/
231+
private static getClassNameByPrototype(target: any): string {
232+
return target.name;
233+
}
234+
235+
/**
236+
* Return name of a class by an instance
237+
*
238+
* @param target - instance of some class
239+
*/
240+
private static getClassNameByInstance(target: any): string {
241+
return Sanitizer.getClassNameByPrototype(target.constructor);
242+
}
243+
244+
/**
245+
* Trim string if it reaches max length
246+
*
247+
* @param target - string to check
248+
*/
249+
private static trimString(target: string): string {
250+
if (target.length > Sanitizer.maxStringLen) {
251+
return target.substr(0, Sanitizer.maxStringLen) + '…';
252+
}
253+
254+
return target;
255+
}
256+
257+
/**
258+
* Represent HTML Element as string with it outer-html
259+
* HTMLDivElement -> "<div ...></div>"
260+
*
261+
* @param target - variable to format
262+
*/
263+
private static formatElement(target: Element): string {
264+
/**
265+
* Also, remove inner HTML because it can be BIG
266+
*/
267+
const innerHTML = target.innerHTML;
268+
269+
if (innerHTML) {
270+
return target.outerHTML.replace(target.innerHTML, '…');
271+
}
272+
273+
return target.outerHTML;
274+
}
275+
276+
/**
277+
* Represent not-constructed class as "<class SomeClass>"
278+
*
279+
* @param target - class to format
280+
*/
281+
private static formatClassPrototype(target: any): string {
282+
const className = Sanitizer.getClassNameByPrototype(target);
283+
284+
return `<class ${className}>`;
285+
}
286+
287+
/**
288+
* Represent a some class instance as a "<instance of SomeClass>"
289+
*
290+
* @param target - class instance to format
291+
*/
292+
private static formatClassInstance(target: any): string {
293+
const className = Sanitizer.getClassNameByInstance(target);
294+
295+
return `<instance of ${className}>`;
296+
}
297+
}

0 commit comments

Comments
 (0)