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,2 +1,4 @@
export type { HawkStorage } from './storages/hawk-storage';
export { HawkUserManager } from './users/hawk-user-manager';
export type { Logger, LogType } from './logger/logger';
export { isLoggerSet, setLogger, resetLogger, log } from './logger/logger';
68 changes: 68 additions & 0 deletions packages/core/src/logger/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* 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;

/**
* Checks if logger instance has been registered.
*/
export function isLoggerSet(): boolean {
return loggerInstance !== 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;
}

/**
* Clears the registered logger instance.
*/
export function resetLogger(): void {
loggerInstance = null;
}

/**
* 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);
}
}
86 changes: 86 additions & 0 deletions packages/core/tests/logger/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';

/**
* Each test gets a fresh module instance via vi.resetModules() so that
* the module-level loggerInstance starts as null.
*/
describe('Logger', () => {
beforeEach(() => {
vi.resetModules();
});

it('should return false from isLoggerSet when no logger has been registered', async () => {
const { isLoggerSet } = await import('../../src/logger/logger');

expect(isLoggerSet()).toBe(false);
});

it('should return true from isLoggerSet after setLogger is called', async () => {
const { isLoggerSet, setLogger } = await import('../../src/logger/logger');

setLogger(vi.fn());

expect(isLoggerSet()).toBe(true);
});

it('should not throw when log is called with no logger registered', async () => {
const { log } = await import('../../src/logger/logger');

expect(() => log('test message')).not.toThrow();
});

it('should forward msg, type, and args to the registered logger', async () => {
const { setLogger, log } = await import('../../src/logger/logger');
const mockLogger = vi.fn();

setLogger(mockLogger);
log('something went wrong', 'warn', { code: 42 });

expect(mockLogger).toHaveBeenCalledOnce();
expect(mockLogger).toHaveBeenCalledWith('something went wrong', 'warn', { code: 42 });
});

it('should pass undefined for omitted type and args', async () => {
const { setLogger, log } = await import('../../src/logger/logger');
const mockLogger = vi.fn();

setLogger(mockLogger);
log('simple');

expect(mockLogger).toHaveBeenCalledWith('simple', undefined, undefined);
});

it('should replace a previously registered logger when setLogger is called again', async () => {
const { setLogger, log } = await import('../../src/logger/logger');
const first = vi.fn();
const second = vi.fn();

setLogger(first);
setLogger(second);
log('msg');

expect(first).not.toHaveBeenCalled();
expect(second).toHaveBeenCalledWith('msg', undefined, undefined);
});

it('should clear the registered logger when resetLogger is called', async () => {
const { isLoggerSet, setLogger, resetLogger } = await import('../../src/logger/logger');

setLogger(vi.fn());
expect(isLoggerSet()).toBe(true);

resetLogger();
expect(isLoggerSet()).toBe(false);
});

it('should become a no-op after resetLogger is called', async () => {
const { setLogger, resetLogger, log } = await import('../../src/logger/logger');
const mockLogger = vi.fn();

setLogger(mockLogger);
resetLogger();
log('msg');

expect(mockLogger).not.toHaveBeenCalled();
});
});
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
13 changes: 10 additions & 3 deletions packages/javascript/src/catcher.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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';
import { id } from './utils/id';
import type {
AffectedUser,
EventContext,
Expand All @@ -18,15 +18,22 @@
import { ConsoleCatcher } from './addons/consoleCatcher';
import { BreadcrumbManager } from './addons/breadcrumbs';
import { validateUser, validateContext, isValidEventPayload } from './utils/validation';
import { HawkUserManager } from '@hawk.so/core';
import { HawkUserManager, setLogger, isLoggerSet, log } from '@hawk.so/core';
import { HawkLocalStorage } from './storages/hawk-local-storage';
import { id } from './utils/id';
import { createBrowserLogger } from "./logger/logger";

Check warning on line 23 in packages/javascript/src/catcher.ts

View workflow job for this annotation

GitHub Actions / lint

Strings must use singlequote

/**
* Allow to use global VERSION, that will be overwritten by Webpack
*/
declare const VERSION: string;

/**
* Registers a global logger instance if not already done.
*/
if (!isLoggerSet()) {
setLogger(createBrowserLogger(VERSION));
}

/**
* Hawk JavaScript Catcher
* Module for errors and exceptions tracking
Expand Down Expand Up @@ -552,12 +559,12 @@
*/
private getUser(): AffectedUser {
const user = this.userManager.getUser();
if (user) {

Check warning on line 562 in packages/javascript/src/catcher.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
return user;
}
const generatedId = id();
this.userManager.persistGeneratedId(generatedId);

Check warning on line 566 in packages/javascript/src/catcher.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
return { id: generatedId };

Check warning on line 567 in packages/javascript/src/catcher.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
}

/**
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/javascript';
* 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');
* setLogger(styledLogger);
* ```
*/
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;
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/storages/hawk-local-storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HawkStorage } from '@hawk.so/core';
import log from '../utils/log.ts';
import { log } from '@hawk.so/core';

/**
* {@link HawkStorage} implementation backed by the browser's {@linkcode localStorage}.
Expand All @@ -11,7 +11,7 @@
return localStorage.getItem(key);
} catch (e) {
log('HawkLocalStorage: getItem failed', 'error', e);
return null;

Check warning on line 14 in packages/javascript/src/storages/hawk-local-storage.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
}
}

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
Loading
Loading