Skip to content

Commit 8070fac

Browse files
committed
refactor(core): Extract shared modules and utilities
- fetchTimer function moved to @hawk.so/core - Sanitizer moved to @hawk.so/core (except Element handling) - SanitizerTypeHandler feature added - browser Element SanitizerTypeHandler added - StackParser moved to @hawk.so/core - Transport moved to @hawk.so/core - event utility functions moved to @hawk.so/core - selector utility functions moved to @hawk.so/core - validation utility functions moved to @hawk.so/core - EventRejectedError moved to @hawk.so/core - suppress error-logs by StackParser in tests
1 parent 4a3d4e2 commit 8070fac

30 files changed

+323
-317
lines changed

packages/core/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@ export type { RandomGenerator } from './utils/random';
33
export { HawkUserManager } from './users/hawk-user-manager';
44
export type { Logger, LogType } from './logger/logger';
55
export { isLoggerSet, setLogger, resetLogger, log } from './logger/logger';
6+
export { Sanitizer } from './modules/sanitizer';
7+
export type { SanitizerTypeHandler } from './modules/sanitizer';
8+
export { StackParser } from './modules/stack-parser';
9+
export { buildElementSelector } from './utils/selector';
10+
export type { Transport } from './transports/transport';
11+
export { EventRejectedError } from './errors';
12+
export { isErrorProcessed, markErrorAsProcessed } from './utils/event';
13+
export { isPlainObject, validateUser, validateContext, isValidEventPayload, isValidBreadcrumb } from './utils/validation';

packages/javascript/src/modules/fetchTimer.ts renamed to packages/core/src/modules/fetch-timer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { log } from '@hawk.so/core';
1+
import { log } from '../logger/logger';
22

33
/**
44
* Sends AJAX request and wait for some time.

packages/javascript/src/modules/sanitizer.ts renamed to packages/core/src/modules/sanitizer.ts

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { isPlainObject } from '../utils/validation';
3+
4+
/**
5+
* Custom type handler for Sanitizer.
6+
*
7+
* Allows user to register their own formatters from external packages.
8+
*/
9+
export interface SanitizerTypeHandler {
10+
/**
11+
* Checks if this handler should be applied to given value
12+
*
13+
* @returns `true`
14+
*/
15+
check: (target: any) => boolean;
16+
17+
/**
18+
* Formats the value into a sanitized representation
19+
*/
20+
format: (target: any) => any;
21+
}
22+
223
/**
324
* This class provides methods for preparing data to sending to Hawk
425
* - trim long strings
5-
* - represent html elements like <div ...> as "<div>" instead of "{}"
626
* - represent big objects as "<big object>"
727
* - represent class as <class SomeClass> or <instance of SomeClass>
828
*/
9-
export default class Sanitizer {
29+
export class Sanitizer {
1030
/**
1131
* Maximum string length
1232
*/
@@ -28,13 +48,31 @@ export default class Sanitizer {
2848
*/
2949
private static readonly maxArrayLength: number = 10;
3050

51+
/**
52+
* Custom type handlers registered via {@link registerHandler}.
53+
*
54+
* Checked in {@link sanitize} before built-in type checks.
55+
*/
56+
private static readonly customHandlers: SanitizerTypeHandler[] = [];
57+
3158
/**
3259
* Check if passed variable is an object
3360
*
3461
* @param target - variable to check
3562
*/
3663
public static isObject(target: any): boolean {
37-
return Sanitizer.typeOf(target) === 'object';
64+
return isPlainObject(target);
65+
}
66+
67+
/**
68+
* Register a custom type handler.
69+
* Handlers are checked before built-in type checks, in reverse registration order
70+
* (last registered = highest priority).
71+
*
72+
* @param handler - handler to register
73+
*/
74+
public static registerHandler(handler: SanitizerTypeHandler): void {
75+
Sanitizer.customHandlers.unshift(handler);
3876
}
3977

4078
/**
@@ -60,19 +98,21 @@ export default class Sanitizer {
6098
*/
6199
if (Sanitizer.isArray(data)) {
62100
return this.sanitizeArray(data, depth + 1, seen);
101+
}
63102

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);
103+
// Check additional handlers provided by env-specific modules or users
104+
// to sanitize some additional cases (e.g. specific object types)
105+
for (const handler of Sanitizer.customHandlers) {
106+
if (handler.check(data)) {
107+
return handler.format(data);
108+
}
109+
}
70110

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)) {
111+
/**
112+
* If values is a not-constructed class, it will be formatted as "<class SomeClass>"
113+
* class Editor {...} -> <class Editor>
114+
*/
115+
if (Sanitizer.isClassPrototype(data)) {
76116
return Sanitizer.formatClassPrototype(data);
77117

78118
/**
@@ -131,7 +171,9 @@ export default class Sanitizer {
131171
* @param depth - current depth of recursion
132172
* @param seen - Set of already seen objects to prevent circular references
133173
*/
134-
private static sanitizeObject(data: { [key: string]: any }, depth: number, seen: WeakSet<object>): Record<string, any> | '<deep object>' | '<big object>' {
174+
private static sanitizeObject(data: {
175+
[key: string]: any
176+
}, depth: number, seen: WeakSet<object>): Record<string, any> | '<deep object>' | '<big object>' {
135177
/**
136178
* If the maximum depth is reached, return a placeholder
137179
*/
@@ -205,24 +247,6 @@ export default class Sanitizer {
205247
return typeof target === 'string';
206248
}
207249

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
219-
*
220-
* @param target - variable to check
221-
*/
222-
private static isElement(target: any): boolean {
223-
return target instanceof Element;
224-
}
225-
226250
/**
227251
* Return name of a passed class
228252
*
@@ -248,31 +272,12 @@ export default class Sanitizer {
248272
*/
249273
private static trimString(target: string): string {
250274
if (target.length > Sanitizer.maxStringLen) {
251-
return target.substr(0, Sanitizer.maxStringLen) + '…';
275+
return target.substring(0, Sanitizer.maxStringLen) + '…';
252276
}
253277

254278
return target;
255279
}
256280

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-
276281
/**
277282
* Represent not-constructed class as "<class SomeClass>"
278283
*

packages/javascript/src/modules/stackParser.ts renamed to packages/core/src/modules/stack-parser.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { StackFrame } from 'error-stack-parser';
22
import ErrorStackParser from 'error-stack-parser';
33
import type { BacktraceFrame, SourceCodeLine } from '@hawk.so/types';
4-
import fetchTimer from './fetchTimer';
4+
import fetchTimer from './fetch-timer';
55

66
/**
77
* This module prepares parsed backtrace
88
*/
9-
export default class StackParser {
9+
export class StackParser {
1010
/**
1111
* Prevents loading one file several times
1212
* name -> content
@@ -48,7 +48,7 @@ export default class StackParser {
4848
try {
4949
if (!frame.fileName) {
5050
return null;
51-
};
51+
}
5252

5353
if (!this.isValidUrl(frame.fileName)) {
5454
return null;
@@ -118,9 +118,9 @@ export default class StackParser {
118118
/**
119119
* Downloads source file
120120
*
121-
* @param {string} fileName - name of file to download
121+
* @param fileName - name of file to download
122122
*/
123-
private async loadSourceFile(fileName): Promise<string | null> {
123+
private async loadSourceFile(fileName: string): Promise<string | null> {
124124
if (this.sourceFilesCache[fileName] !== undefined) {
125125
return this.sourceFilesCache[fileName];
126126
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { CatcherMessage, CatcherMessageType } from '@hawk.so/types';
2+
3+
/**
4+
* Transport interface — anything that can send a CatcherMessage
5+
*/
6+
export interface Transport<T extends CatcherMessageType> {
7+
send(message: CatcherMessage<T>): Promise<void>;
8+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { log } from '@hawk.so/core';
1+
import { log } from '../logger/logger';
22

33
/**
44
* Symbol to mark error as processed by Hawk
55
*/
66
const errorSentShadowProperty = Symbol('__hawk_processed__');
77

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

23-
return error[errorSentShadowProperty] === true;
23+
return (error as Record<symbol, unknown>)[errorSentShadowProperty] === true;
2424
}
2525

2626
/**
@@ -35,7 +35,7 @@ export function markErrorAsProcessed(error: unknown): void {
3535
}
3636

3737
Object.defineProperty(error, errorSentShadowProperty, {
38-
enumerable: false, // Prevent from beight collected by Hawk
38+
enumerable: false, // Prevent from being collected by Hawk
3939
value: true,
4040
writable: true,
4141
configurable: true,

packages/javascript/src/utils/validation.ts renamed to packages/core/src/utils/validation.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { log } from '@hawk.so/core';
2-
import type { AffectedUser, Breadcrumb, EventContext, EventData, JavaScriptAddons } from '@hawk.so/types';
3-
import Sanitizer from '../modules/sanitizer';
1+
import { log } from '../logger/logger';
2+
import type { AffectedUser, Breadcrumb, EventAddons, EventContext, EventData } from '@hawk.so/types';
43

54
/**
65
* Validates user data - basic security checks
76
*
87
* @param user - user data to validate
98
*/
109
export function validateUser(user: AffectedUser): boolean {
11-
if (!user || !Sanitizer.isObject(user)) {
10+
if (!user || !isPlainObject(user)) {
1211
log('validateUser: User must be an object', 'warn');
1312

1413
return false;
@@ -30,7 +29,7 @@ export function validateUser(user: AffectedUser): boolean {
3029
* @param context - context data to validate
3130
*/
3231
export function validateContext(context: EventContext | undefined): boolean {
33-
if (context && !Sanitizer.isObject(context)) {
32+
if (context && !isPlainObject(context)) {
3433
log('validateContext: Context must be an object', 'warn');
3534

3635
return false;
@@ -40,22 +39,23 @@ export function validateContext(context: EventContext | undefined): boolean {
4039
}
4140

4241
/**
43-
* Checks if value is a plain object (not array, Date, etc.)
42+
* Checks if value is a plain object (not null, array, Date, Map, etc.)
4443
*
4544
* @param value - value to check
45+
* @returns `true` if value is a plain object, otherwise `false`
4646
*/
47-
function isPlainObject(value: unknown): value is Record<string, unknown> {
47+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
4848
return Object.prototype.toString.call(value) === '[object Object]';
4949
}
5050

5151
/**
5252
* Runtime check for required EventData fields.
5353
* Per @hawk.so/types EventData, `title` is the only non-optional field.
54-
* Additionally validates `backtrace` shape if present (must be an array).
54+
* Additionally, validates `backtrace` shape if present (must be an array).
5555
*
5656
* @param payload - value to validate
5757
*/
58-
export function isValidEventPayload(payload: unknown): payload is EventData<JavaScriptAddons> {
58+
export function isValidEventPayload(payload: unknown): payload is EventData<EventAddons> {
5959
if (!isPlainObject(payload)) {
6060
return false;
6161
}
@@ -64,11 +64,7 @@ export function isValidEventPayload(payload: unknown): payload is EventData<Java
6464
return false;
6565
}
6666

67-
if (payload.backtrace !== undefined && !Array.isArray(payload.backtrace)) {
68-
return false;
69-
}
70-
71-
return true;
67+
return !(payload.backtrace !== undefined && !Array.isArray(payload.backtrace));
7268
}
7369

7470
/**
@@ -86,9 +82,5 @@ export function isValidBreadcrumb(breadcrumb: unknown): breadcrumb is Breadcrumb
8682
return false;
8783
}
8884

89-
if (breadcrumb.timestamp !== undefined && typeof breadcrumb.timestamp !== 'number') {
90-
return false;
91-
}
92-
93-
return true;
85+
return !(breadcrumb.timestamp !== undefined && typeof breadcrumb.timestamp !== 'number');
9486
}

0 commit comments

Comments
 (0)