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
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { HawkUserManager } from './users/hawk-user-manager';
export type { Logger, LogType } from './logger/logger';
export { isLoggerSet, setLogger, resetLogger, log } from './logger/logger';
export { isPlainObject, validateUser, validateContext, isValidEventPayload, isValidBreadcrumb } from './utils/validation';
export { Sanitizer } from './modules/sanitizer';
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
/* 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 html elements like <div ...> as "<div>" instead of "{}"
* - represent big objects as "<big object>"
* - represent class as <class SomeClass> or <instance of SomeClass>
*/
export default class Sanitizer {
export class Sanitizer {
/**
* Maximum string length
*/
Expand All @@ -28,13 +48,31 @@
*/
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 Sanitizer.typeOf(target) === 'object';
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);
}

/**
Expand All @@ -60,19 +98,21 @@
*/
if (Sanitizer.isArray(data)) {
return this.sanitizeArray(data, depth + 1, seen);
}

/**
* If value is an Element, format it as string with outer HTML
* HTMLDivElement -> "<div ...></div>"
*/
} else if (Sanitizer.isElement(data)) {
return Sanitizer.formatElement(data);
// 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>
*/
} else if (Sanitizer.isClassPrototype(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);

/**
Expand Down Expand Up @@ -131,7 +171,9 @@
* @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>' {
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
*/
Expand Down Expand Up @@ -205,24 +247,6 @@
return typeof target === 'string';
}

/**
* Return string representation of the object type
*
* @param object - object to get type
*/
private static typeOf(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}

/**
* Check if passed variable is an HTML Element
*
* @param target - variable to check
*/
private static isElement(target: any): boolean {
return target instanceof Element;
}

/**
* Return name of a passed class
*
Expand All @@ -248,31 +272,12 @@
*/
private static trimString(target: string): string {
if (target.length > Sanitizer.maxStringLen) {
return target.substr(0, Sanitizer.maxStringLen) + '…';
return target.substring(0, Sanitizer.maxStringLen) + '…';
}

return target;
}

/**
* Represent HTML Element as string with it outer-html
* HTMLDivElement -> "<div ...></div>"
*
* @param target - variable to format
*/
private static formatElement(target: Element): string {
/**
* Also, remove inner HTML because it can be BIG
*/
const innerHTML = target.innerHTML;

if (innerHTML) {
return target.outerHTML.replace(target.innerHTML, '…');
}

return target.outerHTML;
}

/**
* Represent not-constructed class as "<class SomeClass>"
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import Sanitizer from '../../src/modules/sanitizer';
import { describe, expect, it } from 'vitest';
import { Sanitizer } from '../../src';

describe('Sanitizer', () => {
describe('isObject', () => {
Expand Down Expand Up @@ -85,14 +85,6 @@ describe('Sanitizer', () => {
expect(result.a.b.c.d.e).toBe('<deep object>');
});

it('should format HTML elements as a string starting with tag', () => {
const el = document.createElement('div');
const result = Sanitizer.sanitize(el);

expect(typeof result).toBe('string');
expect(result).toMatch(/^<div/);
});

it('should format a class (not constructed) as "<class Name>"', () => {
class Foo {}

Expand Down
3 changes: 1 addition & 2 deletions packages/javascript/src/addons/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
* @file Breadcrumbs module - captures chronological trail of events before an error
*/
import type { Breadcrumb, BreadcrumbLevel, BreadcrumbType, Json, JsonNode } from '@hawk.so/types';
import Sanitizer from '../modules/sanitizer';
import { isValidBreadcrumb, log, Sanitizer } from '@hawk.so/core';
import { buildElementSelector } from '../utils/selector';
import { isValidBreadcrumb, log } from '@hawk.so/core';

/**
* Default maximum number of breadcrumbs to store
Expand Down
2 changes: 1 addition & 1 deletion packages/javascript/src/addons/consoleCatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @file Module for intercepting console logs with stack trace capture
*/
import type { ConsoleLogEvent } from '@hawk.so/types';
import Sanitizer from '../modules/sanitizer';
import { Sanitizer } from '@hawk.so/core';

/**
* Maximum number of console logs to store
Expand Down
14 changes: 11 additions & 3 deletions packages/javascript/src/catcher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import './modules/element-sanitizer';
import Socket from './modules/socket';
import Sanitizer from './modules/sanitizer';
import StackParser from './modules/stackParser';
import type { BreadcrumbsAPI, CatcherMessage, HawkInitialSettings, HawkJavaScriptEvent, Transport } from './types';
import { VueIntegration } from './integrations/vue';
Expand All @@ -18,8 +18,16 @@ import { isErrorProcessed, markErrorAsProcessed } from './utils/event';
import { BrowserRandomGenerator } from './utils/random';
import { ConsoleCatcher } from './addons/consoleCatcher';
import { BreadcrumbManager } from './addons/breadcrumbs';
import { isValidEventPayload, validateContext, validateUser } from '@hawk.so/core';
import { HawkUserManager, isLoggerSet, log, setLogger } from '@hawk.so/core';
import {
HawkUserManager,
isLoggerSet,
isValidEventPayload,
log,
Sanitizer,
setLogger,
validateContext,
validateUser
} from '@hawk.so/core';
import { HawkLocalStorage } from './storages/hawk-local-storage';
import { createBrowserLogger } from './logger/logger';

Expand Down
20 changes: 20 additions & 0 deletions packages/javascript/src/modules/element-sanitizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Sanitizer } from '@hawk.so/core';

/**
* Registers browser-specific sanitizer handler for {@link Element} objects.
*
* Handles HTML Element and represents as string with it outer HTML with
* inner content replaced: HTMLDivElement -> "<div ...></div>"
*/
Sanitizer.registerHandler({
check: (target) => target instanceof Element,
format: (target: Element) => {
const innerHTML = target.innerHTML;

if (innerHTML) {
return target.outerHTML.replace(target.innerHTML, '…');
}

return target.outerHTML;
},
});
26 changes: 26 additions & 0 deletions packages/javascript/tests/modules/element-sanitizer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { Sanitizer } from '@hawk.so/core';
import '../../src/modules/element-sanitizer';

describe('Browser Sanitizer handlers', () => {
describe('Element handler', () => {
it('should format an empty HTML element as its outer HTML', () => {
const el = document.createElement('div');
const result = Sanitizer.sanitize(el);

expect(typeof result).toBe('string');
expect(result).toMatch(/^<div/);
});

it('should replace inner HTML content with ellipsis', () => {
const el = document.createElement('div');

el.innerHTML = '<span>some long content</span>';

const result = Sanitizer.sanitize(el);

expect(result).toContain('…');
expect(result).not.toContain('some long content');
});
});
});
Loading