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
12 changes: 10 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 @@ -33,8 +36,13 @@
"url": "https://github.com/codex-team/hawk.javascript/issues"
},
"homepage": "https://github.com/codex-team/hawk.javascript#readme",
"dependencies": {
"@hawk.so/types": "0.5.8"
},
"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 42 to 47
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.
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export type { HawkStorage } from './storages/hawk-storage';
export { HawkUserManager } from './users/hawk-user-manager';
71 changes: 71 additions & 0 deletions packages/core/src/users/hawk-user-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { AffectedUser } from '@hawk.so/types';
import type { HawkStorage } from '../storages/hawk-storage';

/**
* Storage key used to persist the auto-generated user ID.
*/
export const HAWK_USER_ID_KEY = 'hawk-user-id';

/**
* Manages the affected user identity.
*
* Manually provided users are kept in memory only (they don't change restarts).
* {@link HawkStorage} is used solely to persist the auto-generated ID
* so it survives across sessions.
*/
export class HawkUserManager {
/**
* In-memory user set explicitly via {@link setUser}.

Check warning on line 18 in packages/core/src/users/hawk-user-manager.ts

View workflow job for this annotation

GitHub Actions / lint

The type 'setUser' is undefined
*/
private user: AffectedUser | null = null;

/**
* Underlying storage used to persist auto-generated user ID.
*/
private readonly storage: HawkStorage;

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

/**
* Returns the current affected user, or `null` if none is available.
*
* Priority: in-memory user > persisted user ID.
*/
public getUser(): AffectedUser | null {
if (this.user) {
return this.user;
}
const storedId = this.storage.getItem(HAWK_USER_ID_KEY);
return storedId ? { id: storedId } : null;

Check warning on line 44 in packages/core/src/users/hawk-user-manager.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
}

/**
* Sets the user explicitly (in memory only).
*
* @param user - The affected user provided by the application.
*/
public setUser(user: AffectedUser): void {
this.user = user;
}

/**
* Persists an auto-generated user ID to storage.
*
* @param id - The generated ID to persist.
*/
public persistGeneratedId(id: string): void {
this.storage.setItem(HAWK_USER_ID_KEY, id);
}

/**
* Clears the explicitly set user, falling back to the persisted user ID.
*/
public clear(): void {
this.user = null;
}
}
72 changes: 72 additions & 0 deletions packages/core/tests/users/hawk-user-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { HawkUserManager } from '../../src';
import type { HawkStorage } from '../../src';

describe('HawkUserManager', () => {
let storage: HawkStorage;
let manager: HawkUserManager;

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

it('should return null when no user is set and storage is empty', () => {
expect(manager.getUser()).toBeNull();
});

it('should return in-memory user set via setUser()', () => {
const user = { id: 'user-1', name: 'Ryan Gosling', url: 'https://example.com', photo: 'https://example.com/photo.png' };

manager.setUser(user);

expect(manager.getUser()).toEqual(user);
expect(storage.setItem).not.toHaveBeenCalled();
});

it('should not touch storage when setUser() is called', () => {
manager.setUser({ id: 'user-1' });

expect(storage.setItem).not.toHaveBeenCalled();
expect(storage.removeItem).not.toHaveBeenCalled();
});

it('should return anonymous user from storage when no in-memory user is set', () => {
vi.mocked(storage.getItem).mockReturnValue('anon-123');

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

it('should prefer in-memory user over persisted anonymous ID', () => {
vi.mocked(storage.getItem).mockReturnValue('anon-123');
manager.setUser({ id: 'explicit-user' });

expect(manager.getUser()).toEqual({ id: 'explicit-user' });
});

it('should persist anonymous ID via persistGeneratedId()', () => {
manager.persistGeneratedId('anon-456');

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

it('should clear in-memory user and fall back to persisted anonymous ID', () => {
vi.mocked(storage.getItem).mockReturnValue('anon-123');
manager.setUser({ id: 'user-1' });
manager.clear();

expect(manager.getUser()).toEqual({ id: 'anon-123' });
});

it('should return null after clear() when no anonymous ID is persisted', () => {
manager.setUser({ id: 'user-1' });
manager.clear();

expect(manager.getUser()).toBeNull();
});
});
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'],
},
},
});
57 changes: 23 additions & 34 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,9 @@
import { ConsoleCatcher } from './addons/consoleCatcher';
import { BreadcrumbManager } from './addons/breadcrumbs';
import { validateUser, validateContext, isValidEventPayload } from './utils/validation';
import { HawkUserManager } 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 +64,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 +108,11 @@
*/
private readonly breadcrumbManager: BreadcrumbManager | null;

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

/**
* Catcher constructor
*
Expand All @@ -126,7 +128,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 +193,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?

Copy link
Member Author

Choose a reason for hiding this comment

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

In Catcher.getUser (lazily)

localStorage.setItem(LOCAL_STORAGE_KEY, userId);
}

return {
id: userId,
};
}

/**
* Send test event from client
*/
Expand Down Expand Up @@ -272,14 +255,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 @@ -565,10 +548,16 @@
}

/**
* Current authenticated user
* Returns the current user if set, otherwise generates and persists an anonymous ID.
*/
private getUser(): HawkJavaScriptEvent['user'] {
return this.user || null;
private getUser(): AffectedUser {
const user = this.userManager.getUser();
if (user) {

Check warning on line 555 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 559 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 560 in packages/javascript/src/catcher.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default defineConfig(() => {
fileName: 'hawk',
},
rollupOptions: {
external: ['@hawk.so/core'],
plugins: [
license({
thirdParty: {
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export default defineConfig({
alias: {
'@/types': path.resolve(__dirname, './src/types'),
},
conditions: ['source'],
},
});
3 changes: 3 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -587,8 +587,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "@hawk.so/core@workspace:packages/core"
dependencies:
"@hawk.so/types": "npm:0.5.8"
"@vitest/coverage-v8": "npm:^4.0.18"
vite: "npm:^7.3.1"
vite-plugin-dts: "npm:^4.2.4"
vitest: "npm:^4.0.18"
languageName: unknown
linkType: soft

Expand Down