Skip to content

Commit fe80e08

Browse files
committed
refactor(core): Logger abstraction added
1 parent f727fc0 commit fe80e08

File tree

15 files changed

+321
-65
lines changed

15 files changed

+321
-65
lines changed

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export type { HawkStorage } from './storages/hawk-storage';
22
export type { RandomGenerator } from './utils/random';
33
export { HawkUserManager } from './users/hawk-user-manager';
4+
export type { Logger, LogType } from './logger/logger';
5+
export { isLoggerSet, setLogger, resetLogger, log } from './logger/logger';

packages/core/src/logger/logger.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Log level type for categorizing log messages.
3+
*
4+
* Includes standard console methods supported in both browser and Node.js:
5+
* - Standard levels: `log`, `warn`, `error`, `info`
6+
* - Performance timing: `time`, `timeEnd`
7+
*/
8+
export type LogType = 'log' | 'warn' | 'error' | 'info' | 'time' | 'timeEnd';
9+
10+
/**
11+
* Logger function interface for environment-specific logging implementations.
12+
*
13+
* Implementations should handle message formatting, output styling,
14+
* and platform-specific logging mechanisms (e.g., console, file, network).
15+
*
16+
* @param msg - The message to log.
17+
* @param type - Log level/severity (default: 'log').
18+
* @param args - Additional data to include with the log message.
19+
*/
20+
export interface Logger {
21+
(msg: string, type?: LogType, args?: unknown): void;
22+
}
23+
24+
/**
25+
* Global logger instance, set by environment-specific packages.
26+
*/
27+
let loggerInstance: Logger | null = null;
28+
29+
/**
30+
* Checks if logger instance has been registered.
31+
*/
32+
export function isLoggerSet(): boolean {
33+
return loggerInstance !== null;
34+
}
35+
36+
/**
37+
* Registers the environment-specific logger implementation.
38+
*
39+
* This should be called once during application initialization
40+
* by the environment-specific package.
41+
*
42+
* @param logger - Logger implementation to use globally.
43+
*/
44+
export function setLogger(logger: Logger): void {
45+
loggerInstance = logger;
46+
}
47+
48+
/**
49+
* Clears the registered logger instance.
50+
*/
51+
export function resetLogger(): void {
52+
loggerInstance = null;
53+
}
54+
55+
/**
56+
* Logs a message using the registered logger implementation.
57+
*
58+
* If no logger has been registered via {@link setLogger}, this is a no-op.
59+
*
60+
* @param msg - Message to log.
61+
* @param type - Log level (default: 'log').
62+
* @param args - Additional arguments to log.
63+
*/
64+
export function log(msg: string, type?: LogType, args?: unknown): void {
65+
if (loggerInstance) {
66+
loggerInstance(msg, type, args);
67+
}
68+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
/**
4+
* Each test gets a fresh module instance via vi.resetModules() so that
5+
* the module-level loggerInstance starts as null.
6+
*/
7+
describe('Logger', () => {
8+
beforeEach(() => {
9+
vi.resetModules();
10+
});
11+
12+
it('should return false from isLoggerSet when no logger has been registered', async () => {
13+
const { isLoggerSet } = await import('../../src/logger/logger');
14+
15+
expect(isLoggerSet()).toBe(false);
16+
});
17+
18+
it('should return true from isLoggerSet after setLogger is called', async () => {
19+
const { isLoggerSet, setLogger } = await import('../../src/logger/logger');
20+
21+
setLogger(vi.fn());
22+
23+
expect(isLoggerSet()).toBe(true);
24+
});
25+
26+
it('should not throw when log is called with no logger registered', async () => {
27+
const { log } = await import('../../src/logger/logger');
28+
29+
expect(() => log('test message')).not.toThrow();
30+
});
31+
32+
it('should forward msg, type, and args to the registered logger', async () => {
33+
const { setLogger, log } = await import('../../src/logger/logger');
34+
const mockLogger = vi.fn();
35+
36+
setLogger(mockLogger);
37+
log('something went wrong', 'warn', { code: 42 });
38+
39+
expect(mockLogger).toHaveBeenCalledOnce();
40+
expect(mockLogger).toHaveBeenCalledWith('something went wrong', 'warn', { code: 42 });
41+
});
42+
43+
it('should pass undefined for omitted type and args', async () => {
44+
const { setLogger, log } = await import('../../src/logger/logger');
45+
const mockLogger = vi.fn();
46+
47+
setLogger(mockLogger);
48+
log('simple');
49+
50+
expect(mockLogger).toHaveBeenCalledWith('simple', undefined, undefined);
51+
});
52+
53+
it('should replace a previously registered logger when setLogger is called again', async () => {
54+
const { setLogger, log } = await import('../../src/logger/logger');
55+
const first = vi.fn();
56+
const second = vi.fn();
57+
58+
setLogger(first);
59+
setLogger(second);
60+
log('msg');
61+
62+
expect(first).not.toHaveBeenCalled();
63+
expect(second).toHaveBeenCalledWith('msg', undefined, undefined);
64+
});
65+
66+
it('should clear the registered logger when resetLogger is called', async () => {
67+
const { isLoggerSet, setLogger, resetLogger } = await import('../../src/logger/logger');
68+
69+
setLogger(vi.fn());
70+
expect(isLoggerSet()).toBe(true);
71+
72+
resetLogger();
73+
expect(isLoggerSet()).toBe(false);
74+
});
75+
76+
it('should become a no-op after resetLogger is called', async () => {
77+
const { setLogger, resetLogger, log } = await import('../../src/logger/logger');
78+
const mockLogger = vi.fn();
79+
80+
setLogger(mockLogger);
81+
resetLogger();
82+
log('msg');
83+
84+
expect(mockLogger).not.toHaveBeenCalled();
85+
});
86+
});

packages/javascript/src/addons/breadcrumbs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import type { Breadcrumb, BreadcrumbLevel, BreadcrumbType, Json, JsonNode } from '@hawk.so/types';
55
import Sanitizer from '../modules/sanitizer';
66
import { buildElementSelector } from '../utils/selector';
7-
import log from '../utils/log';
7+
import { log } from '@hawk.so/core';
88
import { isValidBreadcrumb } from '../utils/validation';
99

1010
/**

packages/javascript/src/catcher.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Socket from './modules/socket';
22
import Sanitizer from './modules/sanitizer';
3-
import log from './utils/log';
43
import StackParser from './modules/stackParser';
54
import type { BreadcrumbsAPI, CatcherMessage, HawkInitialSettings, HawkJavaScriptEvent, Transport } from './types';
65
import { VueIntegration } from './integrations/vue';
@@ -20,14 +19,22 @@ import { BrowserRandomGenerator } from './utils/random';
2019
import { ConsoleCatcher } from './addons/consoleCatcher';
2120
import { BreadcrumbManager } from './addons/breadcrumbs';
2221
import { isValidEventPayload, validateContext, validateUser } from './utils/validation';
23-
import { HawkUserManager } from '@hawk.so/core';
22+
import { HawkUserManager, setLogger, isLoggerSet, log } from '@hawk.so/core';
2423
import { HawkLocalStorage } from './storages/hawk-local-storage';
24+
import { createBrowserLogger } from './logger/logger';
2525

2626
/**
2727
* Allow to use global VERSION, that will be overwritten by Webpack
2828
*/
2929
declare const VERSION: string;
3030

31+
/**
32+
* Registers a global logger instance if not already done.
33+
*/
34+
if (!isLoggerSet()) {
35+
setLogger(createBrowserLogger(VERSION));
36+
}
37+
3138
/**
3239
* Hawk JavaScript Catcher
3340
* Module for errors and exceptions tracking
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { Logger, LogType } from '@hawk.so/core';
2+
3+
/**
4+
* Creates a browser console logger with Hawk branding and styled output.
5+
*
6+
* The logger outputs to `window.console` with a dark label badge
7+
* containing the Hawk version. Messages are formatted with CSS
8+
* styling for better visibility in browser developer tools.
9+
*
10+
* @param version - Version string to display in log messages.
11+
* @param style - Optional CSS style for the message text (default: 'color: inherit').
12+
* @returns {Logger} Logger function implementation for browser environments.
13+
*
14+
* @example
15+
* ```TypeScript
16+
* import { createBrowserLogger } from '@hawk.so/javascript';
17+
* import { setLogger } from '@hawk.so/core';
18+
*
19+
* const logger = createBrowserLogger('3.2.0');
20+
* setLogger(logger);
21+
*
22+
* // Custom styling
23+
* const styledLogger = createBrowserLogger('3.2.0', 'color: blue; font-weight: bold');
24+
* setLogger(styledLogger);
25+
* ```
26+
*/
27+
export function createBrowserLogger(version: string, style = 'color: inherit'): Logger {
28+
return (msg: string, type: LogType = 'log', args?: unknown): void => {
29+
if (!('console' in window)) {
30+
return;
31+
}
32+
33+
const editorLabelText = `Hawk (${version})`;
34+
const editorLabelStyle = `line-height: 1em;
35+
color: #fff;
36+
display: inline-block;
37+
background-color: rgba(0,0,0,.7);
38+
padding: 3px 5px;
39+
border-radius: 3px;
40+
margin-right: 2px`;
41+
42+
try {
43+
switch (type) {
44+
case 'time':
45+
case 'timeEnd':
46+
console[type](`( ${editorLabelText} ) ${msg}`);
47+
break;
48+
case 'log':
49+
case 'warn':
50+
case 'error':
51+
case 'info':
52+
if (args !== undefined) {
53+
console[type](`%c${editorLabelText}%c ${msg} %o`, editorLabelStyle, style, args);
54+
} else {
55+
console[type](`%c${editorLabelText}%c ${msg}`, editorLabelStyle, style);
56+
}
57+
break;
58+
}
59+
} catch (ignored) {}
60+
};
61+
}

packages/javascript/src/modules/fetchTimer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import log from '../utils/log';
1+
import { log } from '@hawk.so/core';
22

33
/**
44
* Sends AJAX request and wait for some time.

packages/javascript/src/modules/socket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import log from '../utils/log';
1+
import { log } from '@hawk.so/core';
22
import type { CatcherMessage } from '@/types';
33
import type { Transport } from '../types/transport';
44

packages/javascript/src/storages/hawk-local-storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { HawkStorage } from '@hawk.so/core';
2-
import log from '../utils/log';
2+
import { log } from '@hawk.so/core';
33

44
/**
55
* {@link HawkStorage} implementation backed by the browser's {@linkcode localStorage}.

packages/javascript/src/utils/event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import log from './log';
1+
import { log } from '@hawk.so/core';
22

33
/**
44
* Symbol to mark error as processed by Hawk

0 commit comments

Comments
 (0)