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
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type { HawkStorage } from './storages/hawk-storage';
export type { UserManager } from './users/user-manager';
export { HawkStorageUserManager } from './users/hawk-storage-user-manager';
export type { Logger, LogType } from './logger/logger';
export { setLogger, log } from './logger/logger';
54 changes: 54 additions & 0 deletions packages/core/src/logger/logger.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Comment on lines +37 to +54
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core logger abstraction (setLogger and log functions in @hawk.so/core) lacks test coverage. While the browser-specific implementation (createBrowserLogger) has comprehensive tests, the core abstraction functions are not tested. Consider adding tests to verify that: 1) log is a no-op when no logger is set, 2) setLogger correctly registers a logger, 3) log correctly delegates to the registered logger, and 4) the logger can be replaced by calling setLogger again.

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion packages/javascript/src/addons/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down
12 changes: 7 additions & 5 deletions packages/javascript/src/catcher.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,8 +18,9 @@ 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 { HawkStorageUserManager } from '@hawk.so/core';
import { HawkStorageUserManager, setLogger, log } from '@hawk.so/core';
import { HawkLocalStorage } from './storages/hawk-local-storage';
import { createBrowserLogger } from './logger/logger';
import { id } from './utils/id';

/**
Expand Down Expand Up @@ -120,6 +120,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));
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logger is set on every Catcher instantiation, which means creating multiple Catcher instances will repeatedly call setLogger with the same logger implementation. While this doesn't cause functional issues (since VERSION is constant), it's wasteful. Consider checking if a logger is already set before creating and setting a new one, or set it once at module initialization time. For example: if (!isLoggerSet()) { setLogger(createBrowserLogger(VERSION)); } where isLoggerSet() is a new function exported from @hawk.so/core.

Copilot uses AI. Check for mistakes.

if (typeof settings === 'string') {
settings = {
token: settings,
Expand Down Expand Up @@ -517,7 +519,7 @@ export default class Catcher {
private getIntegrationId(): string {
try {
const decodedIntegrationToken: DecodedIntegrationToken = JSON.parse(atob(this.token));
const {integrationId} = decodedIntegrationToken;
const { integrationId } = decodedIntegrationToken;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting change (adding spaces inside object destructuring) appears to be unrelated to the logger abstraction changes and is a minor style adjustment. While it improves consistency with modern JavaScript style conventions, it would be better to keep such formatting changes separate from functional changes in a PR, or apply them consistently throughout the file if they're part of a deliberate style update.

Suggested change
const { integrationId } = decodedIntegrationToken;
const {integrationId} = decodedIntegrationToken;

Copilot uses AI. Check for mistakes.

if (!integrationId || integrationId === '') {
throw new Error();
Expand Down Expand Up @@ -557,7 +559,7 @@ export default class Catcher {
if (user) {
return user;
}
const newUser: AffectedUser = {id: id()};
const newUser: AffectedUser = { id: id() };
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting change (adding spaces inside object destructuring) appears to be unrelated to the logger abstraction changes and is a minor style adjustment. While it improves consistency with modern JavaScript style conventions, it would be better to keep such formatting changes separate from functional changes in a PR, or apply them consistently throughout the file if they're part of a deliberate style update.

Suggested change
const newUser: AffectedUser = { id: id() };
const newUser: AffectedUser = {id: id()};

Copilot uses AI. Check for mistakes.

this.userManager.setUser(newUser);

Expand Down Expand Up @@ -628,7 +630,7 @@ export default class Catcher {
* @param {Error|string} error — caught error
*/
private getAddons(error: Error | string): HawkJavaScriptEvent['addons'] {
const {innerWidth, innerHeight} = window;
const { innerWidth, innerHeight } = window;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting change (adding spaces inside object destructuring) appears to be unrelated to the logger abstraction changes and is a minor style adjustment. While it improves consistency with modern JavaScript style conventions, it would be better to keep such formatting changes separate from functional changes in a PR, or apply them consistently throughout the file if they're part of a deliberate style update.

Copilot uses AI. Check for mistakes.
const userAgent = window.navigator.userAgent;
const location = window.location.href;
const getParams = this.getGetParams();
Expand Down
61 changes: 61 additions & 0 deletions packages/javascript/src/logger/logger.ts
Original file line number Diff line number Diff line change
@@ -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';
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import path in the documentation example is incorrect. The function is exported from '@hawk.so/javascript', not '@hawk.so/browser'. This could confuse users trying to use the logger. The example should use the correct package name that matches where this code actually lives.

Suggested change
* import { createBrowserLogger } from '@hawk.so/browser';
* import { createBrowserLogger } from '@hawk.so/javascript';

Copilot uses AI. Check for mistakes.
* 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;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS style string contains duplicate 'line-height: 1em;' properties on lines 33 and 36. While this doesn't cause an error, the second declaration will override the first, making the first one redundant. Consider removing one of the duplicate declarations to clean up the code.

Suggested change
line-height: 1em;

Copilot uses AI. Check for mistakes.
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) {}
};
}
2 changes: 1 addition & 1 deletion packages/javascript/src/modules/fetchTimer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import log from '../utils/log';
import { log } from '@hawk.so/core';

/**
* Sends AJAX request and wait for some time.
Expand Down
2 changes: 1 addition & 1 deletion packages/javascript/src/modules/socket.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/javascript/src/utils/event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import log from './log';
import { log } from '@hawk.so/core';

/**
* Symbol to mark error as processed by Hawk
Expand Down
46 changes: 0 additions & 46 deletions packages/javascript/src/utils/log.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/javascript/src/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
18 changes: 9 additions & 9 deletions packages/javascript/tests/breadcrumbs.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
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
(BreadcrumbManager as any).instance = null;
}

describe('BreadcrumbManager', () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
let logSpy: ReturnType<typeof vi.spyOn>;

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', () => {
Expand Down Expand Up @@ -118,15 +119,15 @@ describe('BreadcrumbManager', () => {
});

describe('beforeBreadcrumb', () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
let logSpy: ReturnType<typeof vi.spyOn>;

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', () => {
Expand Down Expand Up @@ -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'
);
});

Expand Down
78 changes: 78 additions & 0 deletions packages/javascript/tests/logger/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
import { createBrowserLogger } from '../../src/logger/logger';

describe('createBrowserLogger', () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;

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();
});
});