Skip to content

Commit af39f39

Browse files
authored
Merge pull request #169 from codex-team/refactor/core-sanitizer
Extract sanitizer module
2 parents 9d58c8a + c605b06 commit af39f39

File tree

10 files changed

+239
-181
lines changed

10 files changed

+239
-181
lines changed

packages/core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export { HawkUserManager } from './users/hawk-user-manager';
44
export type { Logger, LogType } from './logger/logger';
55
export { isLoggerSet, setLogger, resetLogger, log } from './logger/logger';
66
export { validateUser, validateContext, isValidEventPayload, isValidBreadcrumb } from './utils/validation';
7-
export { isPlainObject } from './utils/type-guards';
7+
export { isPlainObject, isArray, isClassPrototype, isClassInstance, isString } from './utils/type-guards';
8+
export { Sanitizer } from './modules/sanitizer';

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

Lines changed: 71 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { isArray, isClassInstance, isClassPrototype, isPlainObject, isString } from '../utils/type-guards';
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
*/
@@ -29,12 +49,21 @@ export default class Sanitizer {
2949
private static readonly maxArrayLength: number = 10;
3050

3151
/**
32-
* Check if passed variable is an object
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+
58+
/**
59+
* Register a custom type handler.
60+
* Handlers are checked before built-in type checks, in reverse registration order
61+
* (last registered = highest priority).
3362
*
34-
* @param target - variable to check
63+
* @param handler - handler to register
3564
*/
36-
public static isObject(target: any): boolean {
37-
return Sanitizer.typeOf(target) === 'object';
65+
public static registerHandler(handler: SanitizerTypeHandler): void {
66+
Sanitizer.customHandlers.unshift(handler);
3867
}
3968

4069
/**
@@ -45,59 +74,50 @@ export default class Sanitizer {
4574
* @param seen - Set of already seen objects to prevent circular references
4675
*/
4776
public static sanitize(data: any, depth = 0, seen = new WeakSet<object>()): any {
48-
/**
49-
* Check for circular references on objects and arrays
50-
*/
77+
// Check for circular references on objects and arrays
5178
if (data !== null && typeof data === 'object') {
5279
if (seen.has(data)) {
5380
return '<circular>';
5481
}
5582
seen.add(data);
5683
}
5784

58-
/**
59-
* If value is an Array, apply sanitizing for each element
60-
*/
61-
if (Sanitizer.isArray(data)) {
85+
// If value is an Array, apply sanitizing for each element
86+
if (isArray(data)) {
6287
return this.sanitizeArray(data, depth + 1, seen);
88+
}
6389

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)) {
90+
// Check additional handlers provided by env-specific modules or users
91+
// to sanitize some additional cases (e.g. specific object types)
92+
for (const handler of Sanitizer.customHandlers) {
93+
if (handler.check(data)) {
94+
return handler.format(data);
95+
}
96+
}
97+
98+
// If values is a not-constructed class, it will be formatted as "<class SomeClass>"
99+
// class Editor {...} -> <class Editor>
100+
if (isClassPrototype(data)) {
76101
return Sanitizer.formatClassPrototype(data);
102+
}
77103

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)) {
104+
// If values is a some class instance, it will be formatted as "<instance of SomeClass>"
105+
// new Editor() -> <instance of Editor>
106+
if (isClassInstance(data)) {
83107
return Sanitizer.formatClassInstance(data);
108+
}
84109

85-
/**
86-
* If values is an object, do recursive call
87-
*/
88-
} else if (Sanitizer.isObject(data)) {
110+
// If values is an object, do recursive call
111+
if (isPlainObject(data)) {
89112
return Sanitizer.sanitizeObject(data, depth + 1, seen);
113+
}
90114

91-
/**
92-
* If values is a string, trim it for max-length
93-
*/
94-
} else if (Sanitizer.isString(data)) {
115+
// If values is a string, trim it for max-length
116+
if (isString(data)) {
95117
return Sanitizer.trimString(data);
96118
}
97119

98-
/**
99-
* If values is a number, boolean and other primitive, leave as is
100-
*/
120+
// If values is a number, boolean and other primitive, leave as is
101121
return data;
102122
}
103123

@@ -109,9 +129,7 @@ export default class Sanitizer {
109129
* @param seen - Set of already seen objects to prevent circular references
110130
*/
111131
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-
*/
132+
// If the maximum length is reached, slice array to max length and add a placeholder
115133
const length = arr.length;
116134

117135
if (length > Sanitizer.maxArrayLength) {
@@ -131,17 +149,18 @@ export default class Sanitizer {
131149
* @param depth - current depth of recursion
132150
* @param seen - Set of already seen objects to prevent circular references
133151
*/
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-
*/
152+
private static sanitizeObject(
153+
data: { [key: string]: any },
154+
depth: number,
155+
seen: WeakSet<object>
156+
): Record<string, any> | '<deep object>' | '<big object>' {
157+
158+
// If the maximum depth is reached, return a placeholder
138159
if (depth > Sanitizer.maxDepth) {
139160
return '<deep object>';
140161
}
141162

142-
/**
143-
* If the object has more keys than the limit, return a placeholder
144-
*/
163+
// If the object has more keys than the limit, return a placeholder
145164
if (Object.keys(data).length > Sanitizer.maxObjectKeysCount) {
146165
return '<big object>';
147166
}
@@ -157,72 +176,6 @@ export default class Sanitizer {
157176
return result;
158177
}
159178

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
219-
*
220-
* @param target - variable to check
221-
*/
222-
private static isElement(target: any): boolean {
223-
return target instanceof Element;
224-
}
225-
226179
/**
227180
* Return name of a passed class
228181
*
@@ -248,31 +201,12 @@ export default class Sanitizer {
248201
*/
249202
private static trimString(target: string): string {
250203
if (target.length > Sanitizer.maxStringLen) {
251-
return target.substr(0, Sanitizer.maxStringLen) + '…';
204+
return target.substring(0, Sanitizer.maxStringLen) + '…';
252205
}
253206

254207
return target;
255208
}
256209

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-
276210
/**
277211
* Represent not-constructed class as "<class SomeClass>"
278212
*

packages/core/src/utils/type-guards.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
13
/**
24
* Checks if value is a plain object (not null, array, Date, Map, etc.)
35
*
@@ -12,3 +14,55 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
1214

1315
return proto === Object.prototype || proto === null;
1416
}
17+
18+
/**
19+
* Check if passed variable is an array
20+
*
21+
* @param value - variable to check
22+
* @returns `true` if value is an array, otherwise `false`
23+
*/
24+
export function isArray(value: any): value is any[] {
25+
return Array.isArray(value);
26+
}
27+
28+
/**
29+
* Check if passed variable is a not-constructed class
30+
*
31+
* @param value - variable to check
32+
* @returns `true` if value is a class prototype, otherwise `false`
33+
*/
34+
export function isClassPrototype(value: any): boolean {
35+
if (!value || !value.constructor) {
36+
return false;
37+
}
38+
39+
/**
40+
* like
41+
* "function Function {
42+
* [native code]
43+
* }"
44+
*/
45+
const constructorStr = value.constructor.toString();
46+
47+
return constructorStr.includes('[native code]') && constructorStr.includes('Function');
48+
}
49+
50+
/**
51+
* Check if passed variable is a constructed class instance
52+
*
53+
* @param value - variable to check
54+
* @returns `true` if value is a class instance, otherwise `false`
55+
*/
56+
export function isClassInstance(value: any): boolean {
57+
return !!(value && value.constructor && (/^class \S+ {/).test(value.constructor.toString()));
58+
}
59+
60+
/**
61+
* Check if passed variable is a string
62+
*
63+
* @param value - variable to check
64+
* @returns `true` if value is a string, otherwise `false`
65+
*/
66+
export function isString(value: any): value is string {
67+
return typeof value === 'string';
68+
}

0 commit comments

Comments
 (0)