diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 66f1fca2..bfb2e940 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,3 +3,5 @@ export type { RandomGenerator } from './utils/random'; export { HawkUserManager } from './users/hawk-user-manager'; export type { Logger, LogType } from './logger/logger'; export { isLoggerSet, setLogger, resetLogger, log } from './logger/logger'; +export { validateUser, validateContext, isValidEventPayload, isValidBreadcrumb } from './utils/validation'; +export { isPlainObject } from './utils/type-guards'; diff --git a/packages/core/src/utils/type-guards.ts b/packages/core/src/utils/type-guards.ts new file mode 100644 index 00000000..3375ee02 --- /dev/null +++ b/packages/core/src/utils/type-guards.ts @@ -0,0 +1,14 @@ +/** + * Checks if value is a plain object (not null, array, Date, Map, etc.) + * + * @param value - value to check + * @returns `true` if value is a plain object, otherwise `false` + */ +export function isPlainObject(value: unknown): value is Record { + if (typeof value !== 'object' || value === null) { + return false; + } + const proto = Object.getPrototypeOf(value); + + return proto === Object.prototype || proto === null; +} diff --git a/packages/javascript/src/utils/validation.ts b/packages/core/src/utils/validation.ts similarity index 64% rename from packages/javascript/src/utils/validation.ts rename to packages/core/src/utils/validation.ts index 293cafc6..e00c3865 100644 --- a/packages/javascript/src/utils/validation.ts +++ b/packages/core/src/utils/validation.ts @@ -1,6 +1,6 @@ -import { log } from '@hawk.so/core'; -import type { AffectedUser, Breadcrumb, EventContext, EventData, JavaScriptAddons } from '@hawk.so/types'; -import Sanitizer from '../modules/sanitizer'; +import { log } from '../logger/logger'; +import type { AffectedUser, Breadcrumb, EventAddons, EventContext, EventData } from '@hawk.so/types'; +import { isPlainObject } from './type-guards'; /** * Validates user data - basic security checks @@ -8,7 +8,7 @@ import Sanitizer from '../modules/sanitizer'; * @param user - user data to validate */ export function validateUser(user: AffectedUser): boolean { - if (!user || !Sanitizer.isObject(user)) { + if (!user || !isPlainObject(user)) { log('validateUser: User must be an object', 'warn'); return false; @@ -30,7 +30,7 @@ export function validateUser(user: AffectedUser): boolean { * @param context - context data to validate */ export function validateContext(context: EventContext | undefined): boolean { - if (context && !Sanitizer.isObject(context)) { + if (context && !isPlainObject(context)) { log('validateContext: Context must be an object', 'warn'); return false; @@ -39,23 +39,14 @@ export function validateContext(context: EventContext | undefined): boolean { return true; } -/** - * Checks if value is a plain object (not array, Date, etc.) - * - * @param value - value to check - */ -function isPlainObject(value: unknown): value is Record { - return Object.prototype.toString.call(value) === '[object Object]'; -} - /** * Runtime check for required EventData fields. * Per @hawk.so/types EventData, `title` is the only non-optional field. - * Additionally validates `backtrace` shape if present (must be an array). + * Additionally, validates `backtrace` shape if present (must be an array). * * @param payload - value to validate */ -export function isValidEventPayload(payload: unknown): payload is EventData { +export function isValidEventPayload(payload: unknown): payload is EventData { if (!isPlainObject(payload)) { return false; } @@ -64,11 +55,10 @@ export function isValidEventPayload(payload: unknown): payload is EventData { + it('should return true for plain object', () => expect(isPlainObject({})).toBe(true)); + it('should return true for plain object with properties', () => expect(isPlainObject({ a: 1 })).toBe(true)); + it('should return false for array', () => expect(isPlainObject([])).toBe(false)); + it('should return false for string', () => expect(isPlainObject('x')).toBe(false)); + it('should return false for number', () => expect(isPlainObject(42)).toBe(false)); + it('should return false for boolean', () => expect(isPlainObject(true)).toBe(false)); + it('should return false for null', () => expect(isPlainObject(null)).toBe(false)); + it('should return false for undefined', () => expect(isPlainObject(undefined)).toBe(false)); + it('should return false for class prototype', () => expect(isPlainObject(Foo)).toBe(false)); + it('should return false for class instance', () => expect(isPlainObject(fooInstance)).toBe(false)); +}); diff --git a/packages/javascript/tests/utils/validation.test.ts b/packages/core/tests/utils/validation.test.ts similarity index 92% rename from packages/javascript/tests/utils/validation.test.ts rename to packages/core/tests/utils/validation.test.ts index 20d02944..bb2a16c2 100644 --- a/packages/javascript/tests/utils/validation.test.ts +++ b/packages/core/tests/utils/validation.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect, vi } from 'vitest'; -import { validateUser, validateContext, isValidEventPayload, isValidBreadcrumb } from '../../src/utils/validation'; +import { describe, expect, it, vi } from 'vitest'; +import { isValidBreadcrumb, isValidEventPayload, validateContext, validateUser } from '../../src'; // Suppress log output produced by log() calls inside validation failures. -vi.mock('@hawk.so/core', () => ({ log: vi.fn(), isLoggerSet: vi.fn(() => true), setLogger: vi.fn() })); +vi.mock('../../src/logger/logger', () => ({ log: vi.fn(), isLoggerSet: vi.fn(() => true), setLogger: vi.fn() })); describe('validateUser', () => { it('should return false when user is null', () => { diff --git a/packages/javascript/src/addons/breadcrumbs.ts b/packages/javascript/src/addons/breadcrumbs.ts index 1e4f0b9b..89ef2e23 100644 --- a/packages/javascript/src/addons/breadcrumbs.ts +++ b/packages/javascript/src/addons/breadcrumbs.ts @@ -4,8 +4,7 @@ import type { Breadcrumb, BreadcrumbLevel, BreadcrumbType, Json, JsonNode } from '@hawk.so/types'; import Sanitizer from '../modules/sanitizer'; import { buildElementSelector } from '../utils/selector'; -import { log } from '@hawk.so/core'; -import { isValidBreadcrumb } from '../utils/validation'; +import { isValidBreadcrumb, log } from '@hawk.so/core'; /** * Default maximum number of breadcrumbs to store diff --git a/packages/javascript/src/catcher.ts b/packages/javascript/src/catcher.ts index 822d974c..5bf07dd1 100644 --- a/packages/javascript/src/catcher.ts +++ b/packages/javascript/src/catcher.ts @@ -18,7 +18,7 @@ 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 './utils/validation'; +import { isValidEventPayload, validateContext, validateUser } from '@hawk.so/core'; import { HawkUserManager, isLoggerSet, log, setLogger } from '@hawk.so/core'; import { HawkLocalStorage } from './storages/hawk-local-storage'; import { createBrowserLogger } from './logger/logger';