Skip to content

Commit 0a0bf23

Browse files
committed
refactor(core): Extract sanitizer module
- Sanitizer moved to @hawk.so/core (except Element handling) - SanitizerTypeHandler feature added - browser Element SanitizerTypeHandler added
1 parent db839db commit 0a0bf23

File tree

7 files changed

+119
-69
lines changed

7 files changed

+119
-69
lines changed

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/tests/modules/sanitizer.test.ts renamed to packages/core/tests/modules/sanitizer.test.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { describe, it, expect } from 'vitest';
2-
import Sanitizer from '../../src/modules/sanitizer';
1+
import { describe, expect, it } from 'vitest';
2+
import { Sanitizer } from '../../src';
33

44
describe('Sanitizer', () => {
55
describe('isObject', () => {
@@ -85,14 +85,6 @@ describe('Sanitizer', () => {
8585
expect(result.a.b.c.d.e).toBe('<deep object>');
8686
});
8787

88-
it('should format HTML elements as a string starting with tag', () => {
89-
const el = document.createElement('div');
90-
const result = Sanitizer.sanitize(el);
91-
92-
expect(typeof result).toBe('string');
93-
expect(result).toMatch(/^<div/);
94-
});
95-
9688
it('should format a class (not constructed) as "<class Name>"', () => {
9789
class Foo {}
9890

packages/javascript/src/addons/breadcrumbs.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
* @file Breadcrumbs module - captures chronological trail of events before an error
33
*/
44
import type { Breadcrumb, BreadcrumbLevel, BreadcrumbType, Json, JsonNode } from '@hawk.so/types';
5-
import Sanitizer from '../modules/sanitizer';
5+
import { isValidBreadcrumb, log, Sanitizer } from '@hawk.so/core';
66
import { buildElementSelector } from '../utils/selector';
7-
import { isValidBreadcrumb, log } from '@hawk.so/core';
87

98
/**
109
* Default maximum number of breadcrumbs to store

packages/javascript/src/addons/consoleCatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @file Module for intercepting console logs with stack trace capture
33
*/
44
import type { ConsoleLogEvent } from '@hawk.so/types';
5-
import Sanitizer from '../modules/sanitizer';
5+
import { Sanitizer } from '@hawk.so/core';
66

77
/**
88
* Maximum number of console logs to store

packages/javascript/src/catcher.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import './modules/element-sanitizer';
12
import Socket from './modules/socket';
2-
import Sanitizer from './modules/sanitizer';
33
import StackParser from './modules/stackParser';
44
import type { BreadcrumbsAPI, CatcherMessage, HawkInitialSettings, HawkJavaScriptEvent, Transport } from './types';
55
import { VueIntegration } from './integrations/vue';
@@ -18,8 +18,16 @@ import { isErrorProcessed, markErrorAsProcessed } from './utils/event';
1818
import { BrowserRandomGenerator } from './utils/random';
1919
import { ConsoleCatcher } from './addons/consoleCatcher';
2020
import { BreadcrumbManager } from './addons/breadcrumbs';
21-
import { isValidEventPayload, validateContext, validateUser } from '@hawk.so/core';
22-
import { HawkUserManager, isLoggerSet, log, setLogger } from '@hawk.so/core';
21+
import {
22+
HawkUserManager,
23+
isLoggerSet,
24+
isValidEventPayload,
25+
log,
26+
Sanitizer,
27+
setLogger,
28+
validateContext,
29+
validateUser
30+
} from '@hawk.so/core';
2331
import { HawkLocalStorage } from './storages/hawk-local-storage';
2432
import { createBrowserLogger } from './logger/logger';
2533

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Sanitizer } from '@hawk.so/core';
2+
3+
/**
4+
* Registers browser-specific sanitizer handler for {@link Element} objects.
5+
*
6+
* Handles HTML Element and represents as string with it outer HTML with
7+
* inner content replaced: HTMLDivElement -> "<div ...></div>"
8+
*/
9+
Sanitizer.registerHandler({
10+
check: (target) => target instanceof Element,
11+
format: (target: Element) => {
12+
const innerHTML = target.innerHTML;
13+
14+
if (innerHTML) {
15+
return target.outerHTML.replace(target.innerHTML, '…');
16+
}
17+
18+
return target.outerHTML;
19+
},
20+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { Sanitizer } from '@hawk.so/core';
3+
import '../../src/modules/element-sanitizer';
4+
5+
describe('Browser Sanitizer handlers', () => {
6+
describe('Element handler', () => {
7+
it('should format an empty HTML element as its outer HTML', () => {
8+
const el = document.createElement('div');
9+
const result = Sanitizer.sanitize(el);
10+
11+
expect(typeof result).toBe('string');
12+
expect(result).toMatch(/^<div/);
13+
});
14+
15+
it('should replace inner HTML content with ellipsis', () => {
16+
const el = document.createElement('div');
17+
18+
el.innerHTML = '<span>some long content</span>';
19+
20+
const result = Sanitizer.sanitize(el);
21+
22+
expect(result).toContain('…');
23+
expect(result).not.toContain('some long content');
24+
});
25+
});
26+
});

0 commit comments

Comments
 (0)