diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 2e182cf..ec61bae 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1 +1,2 @@ export { HawkLocalStorage } from './storages/hawk-local-storage'; +export { createBrowserLogger } from './utils/logger'; diff --git a/packages/browser/src/utils/logger.ts b/packages/browser/src/utils/logger.ts new file mode 100644 index 0000000..9c39261 --- /dev/null +++ b/packages/browser/src/utils/logger.ts @@ -0,0 +1,61 @@ +import type { Logger, LogType } from '@hawk.so/core'; + +/** + * Creates a browser console logger with Hawk branding and styled output. + * + * The logger outputs to `window.console` with a dark label badge + * containing the Hawk version. Messages are formatted with CSS + * styling for better visibility in browser developer tools. + * + * @param version - Version string to display in log messages. + * @param style - Optional CSS style for the message text (default: 'color: inherit'). + * @returns {Logger} Logger function implementation for browser environments. + * + * @example + * ```TypeScript + * import { createBrowserLogger } from '@hawk.so/browser'; + * import { setLogger } from '@hawk.so/core'; + * + * const logger = createBrowserLogger('3.2.0'); + * setLogger(logger); + * + * // Custom styling + * const styledLogger = createBrowserLogger('3.2.0', 'color: blue; font-weight: bold'); + * ``` + */ +export function createBrowserLogger(version: string, style = 'color: inherit'): Logger { + return (msg: string, type: LogType = 'log', args?: unknown): void => { + if (!('console' in window)) { + return; + } + + const editorLabelText = `Hawk (${version})`; + const editorLabelStyle = `line-height: 1em; + color: #fff; + display: inline-block; + line-height: 1em; + background-color: rgba(0,0,0,.7); + padding: 3px 5px; + border-radius: 3px; + margin-right: 2px`; + + try { + switch (type) { + case 'time': + case 'timeEnd': + console[type](`( ${editorLabelText} ) ${msg}`); + break; + case 'log': + case 'warn': + case 'error': + case 'info': + if (args !== undefined) { + console[type](`%c${editorLabelText}%c ${msg} %o`, editorLabelStyle, style, args); + } else { + console[type](`%c${editorLabelText}%c ${msg}`, editorLabelStyle, style); + } + break; + } + } catch (ignored) {} + }; +} diff --git a/packages/browser/tests/logger.test.ts b/packages/browser/tests/logger.test.ts new file mode 100644 index 0000000..b8d63fe --- /dev/null +++ b/packages/browser/tests/logger.test.ts @@ -0,0 +1,78 @@ +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import { createBrowserLogger } from '../src'; + +describe('createBrowserLogger', () => { + let consoleLogSpy: ReturnType; + let consoleWarnSpy: ReturnType; + let consoleErrorSpy: ReturnType; + + beforeEach(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should log message with default type', () => { + const logger = createBrowserLogger('1.0.0'); + + logger('Test message'); + + expect(consoleLogSpy).toHaveBeenCalledWith( + '%cHawk (1.0.0)%c Test message', + expect.stringContaining('background-color'), + 'color: inherit' + ); + }); + + it('should log message with specified type', () => { + const logger = createBrowserLogger('2.0.0'); + + logger('Warning message', 'warn'); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + '%cHawk (2.0.0)%c Warning message', + expect.stringContaining('background-color'), + 'color: inherit' + ); + }); + + it('should log error with args', () => { + const logger = createBrowserLogger('3.0.0'); + const errorObj = new Error('Test error'); + + logger('Error occurred', 'error', errorObj); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + '%cHawk (3.0.0)%c Error occurred %o', + expect.stringContaining('background-color'), + 'color: inherit', + errorObj + ); + }); + + it('should handle time/timeEnd types', () => { + const consoleTimeSpy = vi.spyOn(console, 'time').mockImplementation(() => {}); + const logger = createBrowserLogger('4.0.0'); + + logger('Timer started', 'time'); + + expect(consoleTimeSpy).toHaveBeenCalledWith( + expect.stringContaining('Hawk (4.0.0)') + ); + + consoleTimeSpy.mockRestore(); + }); + + it('should not throw when console method is unavailable', () => { + const logger = createBrowserLogger('5.0.0'); + + expect(() => { + // @ts-expect-error - testing invalid type + logger('Test', 'invalidType'); + }).not.toThrow(); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 880e71d..e93a7b4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,5 @@ export type { HawkStorage } from './storages/hawk-storage'; export type { UserManager } from './users/user-manager'; export { StorageUserManager } from './users/storage-user-manager'; +export type { Logger, LogType } from './utils/logger'; +export { setLogger, log } from './utils/logger'; diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts new file mode 100644 index 0000000..852a903 --- /dev/null +++ b/packages/core/src/utils/logger.ts @@ -0,0 +1,54 @@ +/** + * Log level type for categorizing log messages. + * + * Includes standard console methods supported in both browser and Node.js: + * - Standard levels: `log`, `warn`, `error`, `info` + * - Performance timing: `time`, `timeEnd` + */ +export type LogType = 'log' | 'warn' | 'error' | 'info' | 'time' | 'timeEnd'; + +/** + * Logger function interface for environment-specific logging implementations. + * + * Implementations should handle message formatting, output styling, + * and platform-specific logging mechanisms (e.g., console, file, network). + * + * @param msg - The message to log. + * @param type - Log level/severity (default: 'log'). + * @param args - Additional data to include with the log message. + */ +export interface Logger { + (msg: string, type?: LogType, args?: unknown): void; +} + +/** + * Global logger instance, set by environment-specific packages. + */ +let loggerInstance: Logger | null = null; + +/** + * Registers the environment-specific logger implementation. + * + * This should be called once during application initialization + * by the environment-specific package. + * + * @param logger - Logger implementation to use globally. + */ +export function setLogger(logger: Logger): void { + loggerInstance = logger; +} + +/** + * Logs a message using the registered logger implementation. + * + * If no logger has been registered via {@link setLogger}, this is a no-op. + * + * @param msg - Message to log. + * @param type - Log level (default: 'log'). + * @param args - Additional arguments to log. + */ +export function log(msg: string, type?: LogType, args?: unknown): void { + if (loggerInstance) { + loggerInstance(msg, type, args); + } +} diff --git a/packages/javascript/src/addons/breadcrumbs.ts b/packages/javascript/src/addons/breadcrumbs.ts index faa14a8..1e4f0b9 100644 --- a/packages/javascript/src/addons/breadcrumbs.ts +++ b/packages/javascript/src/addons/breadcrumbs.ts @@ -4,7 +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 '../utils/log'; +import { log } from '@hawk.so/core'; import { isValidBreadcrumb } from '../utils/validation'; /** diff --git a/packages/javascript/src/catcher.ts b/packages/javascript/src/catcher.ts index b7163f5..b8af671 100644 --- a/packages/javascript/src/catcher.ts +++ b/packages/javascript/src/catcher.ts @@ -1,6 +1,5 @@ import Socket from './modules/socket'; import Sanitizer from './modules/sanitizer'; -import log from './utils/log'; import StackParser from './modules/stackParser'; import type { CatcherMessage, HawkInitialSettings, BreadcrumbsAPI, Transport } from './types'; import { VueIntegration } from './integrations/vue'; @@ -19,8 +18,8 @@ import { ConsoleCatcher } from './addons/consoleCatcher'; import { BreadcrumbManager } from './addons/breadcrumbs'; import { validateUser, validateContext, isValidEventPayload } from './utils/validation'; import type { UserManager } from '@hawk.so/core'; -import { StorageUserManager } from '@hawk.so/core'; -import { HawkLocalStorage } from '@hawk.so/browser'; +import { StorageUserManager, setLogger, log } from '@hawk.so/core'; +import { HawkLocalStorage, createBrowserLogger } from '@hawk.so/browser'; import { id } from './utils/id'; /** @@ -120,6 +119,8 @@ export default class Catcher { * @param {HawkInitialSettings|string} settings - If settings is a string, it means an Integration Token */ constructor(settings: HawkInitialSettings | string) { + setLogger(createBrowserLogger(VERSION)); + if (typeof settings === 'string') { settings = { token: settings, diff --git a/packages/javascript/src/modules/fetchTimer.ts b/packages/javascript/src/modules/fetchTimer.ts index bd32bd6..a17c1d4 100644 --- a/packages/javascript/src/modules/fetchTimer.ts +++ b/packages/javascript/src/modules/fetchTimer.ts @@ -1,4 +1,4 @@ -import log from '../utils/log'; +import { log } from '@hawk.so/core'; /** * Sends AJAX request and wait for some time. diff --git a/packages/javascript/src/modules/socket.ts b/packages/javascript/src/modules/socket.ts index eb64b59..cbc662a 100644 --- a/packages/javascript/src/modules/socket.ts +++ b/packages/javascript/src/modules/socket.ts @@ -1,4 +1,4 @@ -import log from '../utils/log'; +import { log } from '@hawk.so/core'; import type { CatcherMessage } from '@/types'; import type { Transport } from '../types/transport'; diff --git a/packages/javascript/src/utils/event.ts b/packages/javascript/src/utils/event.ts index 3882c27..6374153 100644 --- a/packages/javascript/src/utils/event.ts +++ b/packages/javascript/src/utils/event.ts @@ -1,4 +1,4 @@ -import log from './log'; +import { log } from '@hawk.so/core'; /** * Symbol to mark error as processed by Hawk diff --git a/packages/javascript/src/utils/log.ts b/packages/javascript/src/utils/log.ts deleted file mode 100644 index 4087d90..0000000 --- a/packages/javascript/src/utils/log.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Logger module - * - * @example - * log('We got an error', 'error') - */ - -/** - * Allow to use global VERSION, that will be overwritten by Webpack - */ -declare const VERSION: string; - -/** - * Custom logger - * - * @param {string} msg - message - * @param {string} type - logging type 'log'|'warn'|'error'|'info' - * @param {*} [args] - argument to log with a message - * @param {string} style - additional styling to message - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default function log(msg: string, type = 'log', args?: any, style = 'color: inherit'): void { - if (!('console' in window) || !window.console[type]) { - return; - } - - const editorLabelText = `Hawk (${VERSION})`; - const editorLabelStyle = `line-height: 1em; - color: #fff; - display: inline-block; - line-height: 1em; - background-color: rgba(0,0,0,.7); - padding: 3px 5px; - border-radius: 3px; - margin-right: 2px`; - - try { - if (['time', 'timeEnd'].includes(type)) { - console[type](`( ${editorLabelText} ) ${msg}`); - } else if (args) { - console[type](`%c${editorLabelText}%c ${msg} %o`, editorLabelStyle, style, args); - } else { - console[type](`%c${editorLabelText}%c ${msg}`, editorLabelStyle, style); - } - } catch (ignored) {} -} diff --git a/packages/javascript/src/utils/validation.ts b/packages/javascript/src/utils/validation.ts index c0f9f66..293cafc 100644 --- a/packages/javascript/src/utils/validation.ts +++ b/packages/javascript/src/utils/validation.ts @@ -1,4 +1,4 @@ -import log from './log'; +import { log } from '@hawk.so/core'; import type { AffectedUser, Breadcrumb, EventContext, EventData, JavaScriptAddons } from '@hawk.so/types'; import Sanitizer from '../modules/sanitizer'; diff --git a/packages/javascript/tests/breadcrumbs.test.ts b/packages/javascript/tests/breadcrumbs.test.ts index f02d7dc..7afec05 100644 --- a/packages/javascript/tests/breadcrumbs.test.ts +++ b/packages/javascript/tests/breadcrumbs.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { BreadcrumbManager } from '../src/addons/breadcrumbs'; import type { Breadcrumb } from '@hawk.so/types'; +import * as core from '@hawk.so/core'; function resetManager(): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -8,15 +9,15 @@ function resetManager(): void { } describe('BreadcrumbManager', () => { - let warnSpy: ReturnType; + let logSpy: ReturnType; beforeEach(() => { resetManager(); - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + logSpy = vi.spyOn(core, 'log').mockImplementation(() => {}); }); afterEach(() => { - warnSpy.mockRestore(); + logSpy.mockRestore(); }); it('should return empty array when no breadcrumbs added', () => { @@ -118,15 +119,15 @@ describe('BreadcrumbManager', () => { }); describe('beforeBreadcrumb', () => { - let warnSpy: ReturnType; + let logSpy: ReturnType; beforeEach(() => { resetManager(); - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + logSpy = vi.spyOn(core, 'log').mockImplementation(() => {}); }); afterEach(() => { - warnSpy.mockRestore(); + logSpy.mockRestore(); }); it('should store modified breadcrumb when hook returns changed object', () => { @@ -183,10 +184,9 @@ describe('beforeBreadcrumb', () => { // Assert expect(m.getBreadcrumbs()[0].message).toBe('original'); - expect(warnSpy).toHaveBeenCalledWith( + expect(logSpy).toHaveBeenCalledWith( expect.stringContaining('Invalid beforeBreadcrumb value'), - expect.anything(), - expect.anything() + 'warn' ); });