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
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ jobs:
CI_JOB_NUMBER: 2
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 0
- name: Use Node.js from .nvmrc
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- run: corepack enable
- run: yarn install
- run: yarn workspace @hawk.so/javascript test
- run: yarn test:modified origin/${{ github.event.pull_request.base.ref }}

build:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"dev": "yarn workspace @hawk.so/javascript dev",
"build:all": "yarn workspaces foreach -Apt run build",
"build:modified": "yarn workspaces foreach --since=\"$@\" -Rpt run build",
"test:all": "yarn workspaces foreach -Apt run test",
"test:modified": "yarn workspaces foreach --since=\"$@\" -Rpt run test",
"stats": "yarn workspace @hawk.so/javascript stats",
"lint": "eslint -c ./.eslintrc.cjs packages/*/src --ext .ts,.js --fix",
"lint-test": "eslint -c ./.eslintrc.cjs packages/*/src --ext .ts,.js"
Expand Down
9 changes: 7 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
}
},
"scripts": {
"build": "vite build"
"build": "vite build",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "eslint --fix \"src/**/*.{js,ts}\""
},
"repository": {
"type": "git",
Expand All @@ -34,7 +37,9 @@
},
"homepage": "https://github.com/codex-team/hawk.javascript#readme",
"devDependencies": {
"@vitest/coverage-v8": "^4.0.18",
"vite": "^7.3.1",
"vite-plugin-dts": "^4.2.4"
"vite-plugin-dts": "^4.2.4",
"vitest": "^4.0.18"
}
Comment on lines 39 to 44
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 @hawk.so/core package imports AffectedUser from '@hawk.so/types' but this dependency is not listed in the package.json. This will cause build and runtime errors. Add '@hawk.so/types': 'npm:0.5.8' to the dependencies or peerDependencies section of packages/core/package.json.

Copilot uses AI. Check for mistakes.
}
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { HawkStorage } from './storages/hawk-storage';
export type { UserManager } from './users/user-manager';
export { HawkStorageUserManager } from './users/hawk-storage-user-manager';
26 changes: 26 additions & 0 deletions packages/core/src/storages/hawk-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Abstract key–value storage contract used by Hawk internals to persist data across sessions.
*/
export interface HawkStorage {
/**
* Returns the value associated with the given key, or `null` if none exists.
*
* @param key - Storage key to look up.
*/
getItem(key: string): string | null

/**
* Persists a value under the given key.
*
* @param key - Storage key.
* @param value - Value to store.
*/
setItem(key: string, value: string): void

/**
* Removes the entry for the given key.
*
* @param key - Storage key to remove.
*/
removeItem(key: string): void
}
49 changes: 49 additions & 0 deletions packages/core/src/users/hawk-storage-user-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { AffectedUser } from '@hawk.so/types';
import type { HawkStorage } from '../storages/hawk-storage';
import type { UserManager } from './user-manager';

/**
* Storage key used to persist the user identifier.
*/
const HAWK_USER_STORAGE_KEY = 'hawk-user-id';

/**
* {@link UserManager} implementation that persists the affected user
* via an injected {@link HawkStorage} backend.
*/
export class HawkStorageUserManager implements UserManager {
Copy link
Member

Choose a reason for hiding this comment

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

its not clear for me why do we need this class? Maybe UserManager can do the same (accept HawkStorage as an argument and implement getUser/setUser etc)

/**
* Underlying storage used to read and write the user identifier.
*/
private readonly storage: HawkStorage;

/**
* @param storage - Storage backend to use for persistence.
*/
constructor(storage: HawkStorage) {
this.storage = storage;
}

/** @inheritDoc */
public getUser(): AffectedUser | null {
const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY);

if (storedId) {
return {
id: storedId,
};
}

return null;
Comment on lines +29 to +37
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 getUser() method doesn't handle potential errors from storage.getItem(). If the storage is unavailable or throws an error (e.g., SecurityError in private browsing mode), this could crash the application. Consider wrapping the storage access in a try-catch block and returning null on error.

Suggested change
const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY);
if (storedId) {
return {
id: storedId,
};
}
return null;
try {
const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY);
if (storedId) {
return {
id: storedId,
};
}
return null;
} catch {
return null;
}

Copilot uses AI. Check for mistakes.
}
Comment on lines +28 to +38
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 method only reconstructs the user object with the id field from storage, losing any other AffectedUser properties that were originally set. This is inconsistent with the setUser method which accepts a full AffectedUser object. See also the comment on the setUser method.

Copilot uses AI. Check for mistakes.

/** @inheritDoc */
public setUser(user: AffectedUser): void {
this.storage.setItem(HAWK_USER_STORAGE_KEY, user.id);
Comment on lines +29 to +42
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 HawkStorageUserManager only persists the user.id field and loses any other AffectedUser properties (such as name, email, url, image, or any custom fields that AffectedUser might support). When getUser() is called, it only reconstructs the id field. This means if a developer calls setUser with a full user object containing additional properties, those properties will be lost. Consider serializing the entire AffectedUser object as JSON, or document this limitation clearly if it's intentional.

Suggested change
const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY);
if (storedId) {
return {
id: storedId,
};
}
return null;
}
/** @inheritDoc */
public setUser(user: AffectedUser): void {
this.storage.setItem(HAWK_USER_STORAGE_KEY, user.id);
const storedValue = this.storage.getItem(HAWK_USER_STORAGE_KEY);
if (!storedValue) {
return null;
}
// Try to parse the stored value as a full AffectedUser object.
try {
const parsed = JSON.parse(storedValue) as AffectedUser | null;
if (parsed && typeof parsed === 'object' && 'id' in parsed) {
return parsed;
}
} catch {
// Ignore JSON parsing errors and fall back to legacy behavior below.
}
// Backward compatibility: prior versions stored only the user id as a string.
return {
id: storedValue,
};
}
/** @inheritDoc */
public setUser(user: AffectedUser): void {
// Persist the full AffectedUser object so that all properties are retained.
this.storage.setItem(HAWK_USER_STORAGE_KEY, JSON.stringify(user));

Copilot uses AI. Check for mistakes.
}

/** @inheritDoc */
public clear(): void {
this.storage.removeItem(HAWK_USER_STORAGE_KEY);
}
Comment on lines +41 to +48
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 setUser() and clear() methods don't handle potential errors from storage operations. If the storage is unavailable or throws an error (e.g., QuotaExceededError or SecurityError), this could crash the application. Consider wrapping the storage operations in a try-catch block and handling errors gracefully (e.g., logging them without crashing).

Copilot uses AI. Check for mistakes.
}
26 changes: 26 additions & 0 deletions packages/core/src/users/user-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { AffectedUser } from '@hawk.so/types';

/**
* Contract for user identity managers.
*
* Implementations are responsible for persisting and retrieving the
* {@link AffectedUser} that is attached to every error report sent by the catcher.
*/
export interface UserManager {
/**
* Returns the current affected user, or `null` if none has been set.
*/
getUser(): AffectedUser | null

/**
* Replaces the stored user with the provided one.
*
* @param user - The affected user to persist.
*/
setUser(user: AffectedUser): void

/**
* Removes any previously stored user data.
*/
clear(): void
}
41 changes: 41 additions & 0 deletions packages/core/tests/users/hawk-storage-user-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { HawkStorageUserManager } from '../../src';
import type { HawkStorage } from '../../src';

describe('StorageUserManager', () => {
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 test suite is named 'StorageUserManager' but the class being tested is named 'HawkStorageUserManager'. Update the describe block name to match the actual class name for clarity and consistency.

Suggested change
describe('StorageUserManager', () => {
describe('HawkStorageUserManager', () => {

Copilot uses AI. Check for mistakes.
let storage: HawkStorage;
let manager: HawkStorageUserManager;

beforeEach(() => {
storage = {
getItem: vi.fn().mockReturnValue(null),
setItem: vi.fn(),
removeItem: vi.fn(),
};
manager = new HawkStorageUserManager(storage);
});

it('should return null when storage is empty', () => {
expect(manager.getUser()).toBeNull();
expect(storage.getItem).toHaveBeenCalledWith('hawk-user-id');
});

it('should return user when ID exists in storage', () => {
vi.mocked(storage.getItem).mockReturnValue('test-user-123');

expect(manager.getUser()).toEqual({id: 'test-user-123'});
expect(storage.getItem).toHaveBeenCalledWith('hawk-user-id');
});

it('should persist user ID via setUser()', () => {
manager.setUser({id: 'user-abc'});

expect(storage.setItem).toHaveBeenCalledWith('hawk-user-id', 'user-abc');
});

it('should remove user ID via clear()', () => {
manager.clear();

expect(storage.removeItem).toHaveBeenCalledWith('hawk-user-id');
});
});
13 changes: 13 additions & 0 deletions packages/core/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": null,
"declaration": false,
"types": ["vitest/globals"]
},
"include": [
"src/**/*",
"tests/**/*",
"vitest.config.ts"
]
}
15 changes: 15 additions & 0 deletions packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
include: ['tests/**/*.test.ts'],
typecheck: {
tsconfig: './tsconfig.test.json',
},
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
},
},
});
3 changes: 3 additions & 0 deletions packages/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build": "vite build",
"stats": "size-limit > stats.txt",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"lint": "eslint --fix \"src/**/*.{js,ts}\""
},
Expand All @@ -39,10 +40,12 @@
},
"homepage": "https://github.com/codex-team/hawk.javascript#readme",
"dependencies": {
"@hawk.so/core": "workspace:^",
"error-stack-parser": "^2.1.4"
},
"devDependencies": {
"@hawk.so/types": "0.5.8",
"@vitest/coverage-v8": "^4.0.18",
"jsdom": "^28.0.0",
"vite": "^7.3.1",
"vite-plugin-dts": "^4.2.4",
Expand Down
65 changes: 29 additions & 36 deletions packages/javascript/src/catcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
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 @@ -19,6 +18,10 @@
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 { HawkLocalStorage } from './storages/hawk-local-storage';
import { id } from './utils/id';

/**
* Allow to use global VERSION, that will be overwritten by Webpack
Expand Down Expand Up @@ -62,11 +65,6 @@
*/
private readonly release: string | undefined;

/**
* Current authenticated user
*/
private user: AffectedUser;

/**
* Any additional data passed by user for sending with all messages
*/
Expand Down Expand Up @@ -111,6 +109,11 @@
*/
private readonly breadcrumbManager: BreadcrumbManager | null;

/**
* Current authenticated user manager instance
*/
private readonly userManager: UserManager = new HawkStorageUserManager(new HawkLocalStorage());

/**
* Catcher constructor
*
Expand All @@ -126,7 +129,9 @@
this.token = settings.token;
this.debug = settings.debug || false;
this.release = settings.release !== undefined ? String(settings.release) : undefined;
this.setUser(settings.user || Catcher.getGeneratedUser());
if (settings.user) {
this.setUser(settings.user);
}
this.setContext(settings.context || undefined);
this.beforeSend = settings.beforeSend;
this.disableVueErrorHandler =
Expand Down Expand Up @@ -189,27 +194,6 @@
}
}

/**
* Generates user if no one provided via HawkCatcher settings
* After generating, stores user for feature requests
*/
private static getGeneratedUser(): AffectedUser {
let userId: string;
const LOCAL_STORAGE_KEY = 'hawk-user-id';
const storedId = localStorage.getItem(LOCAL_STORAGE_KEY);

if (storedId) {
userId = storedId;
} else {
userId = id();
Copy link
Member

Choose a reason for hiding this comment

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

where we create the default id now?

localStorage.setItem(LOCAL_STORAGE_KEY, userId);
}

return {
id: userId,
};
}

/**
* Send test event from client
*/
Expand Down Expand Up @@ -272,14 +256,14 @@
return;
}

this.user = user;
this.userManager.setUser(user);
}

/**
* Clear current user information (revert to generated user)
* Clear current user information
*/
public clearUser(): void {
this.user = Catcher.getGeneratedUser();
this.userManager.clear();
}

/**
Expand Down Expand Up @@ -533,7 +517,7 @@
private getIntegrationId(): string {
try {
const decodedIntegrationToken: DecodedIntegrationToken = JSON.parse(atob(this.token));
const { integrationId } = decodedIntegrationToken;
const {integrationId} = decodedIntegrationToken;

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

View workflow job for this annotation

GitHub Actions / lint

A space is required before '}'

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

View workflow job for this annotation

GitHub Actions / lint

A space is required after '{'
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 spacing in object destructuring was changed from const { integrationId } to const {integrationId} (no spaces). However, other parts of the codebase use spaces in destructuring (e.g., packages/javascript/src/addons/consoleCatcher.ts lines 258-259). This creates inconsistency. Consider keeping the original spacing to maintain consistency with the rest of the codebase.

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

Copilot uses AI. Check for mistakes.

if (!integrationId || integrationId === '') {
throw new Error();
Expand Down Expand Up @@ -565,10 +549,19 @@
}

/**
* Current authenticated user
* Returns the current user if exists, otherwise creates and persists a new one.
*/
private getUser(): HawkJavaScriptEvent['user'] {
return this.user || null;
private getUser(): AffectedUser {
const user = this.userManager.getUser();

if (user) {
return user;
}
const newUser: AffectedUser = {id: id()};

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

View workflow job for this annotation

GitHub Actions / lint

A space is required before '}'

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

View workflow job for this annotation

GitHub Actions / lint

A space is required after '{'

this.userManager.setUser(newUser);

return newUser;
}
Comment on lines +554 to 565
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 getUser() method now always returns a non-null AffectedUser (by creating and persisting one if none exists), which is a semantic change from the previous implementation. While the HawkJavaScriptEvent type allows user to be AffectedUser | null, the user field will now never be null in practice. Consider updating the event type definition to reflect this guarantee (user: AffectedUser) since a user is always generated and present, or document this behavioral change clearly.

Copilot uses AI. Check for mistakes.
Comment on lines +554 to 565
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 new user management implementation changes behavior: users are now generated lazily on first getUser() call instead of eagerly in the constructor. However, there are no tests in packages/javascript/tests verifying this new integration behavior, such as verifying that a user is automatically created when an error is sent without explicitly setting one. Consider adding tests to verify the lazy user generation behavior and the integration with UserManager.

Copilot uses AI. Check for mistakes.

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

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

View workflow job for this annotation

GitHub Actions / lint

A space is required before '}'

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

View workflow job for this annotation

GitHub Actions / lint

A space is required after '{'
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 spacing in object destructuring was changed from const { innerWidth, innerHeight } to const {innerWidth, innerHeight} (no spaces). However, other parts of the codebase use spaces in destructuring (e.g., packages/javascript/src/addons/consoleCatcher.ts lines 258-259). This creates inconsistency. Consider keeping the original spacing to maintain consistency with the rest of the codebase.

Suggested change
const {innerWidth, innerHeight} = window;
const { innerWidth, innerHeight } = window;

Copilot uses AI. Check for mistakes.
const userAgent = window.navigator.userAgent;
const location = window.location.href;
const getParams = this.getGetParams();
Expand Down
Loading