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
6 changes: 5 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
},
"scripts": {
"build": "vite build",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "eslint --fix \"src/**/*.{js,ts}\""
},
"repository": {
Expand All @@ -35,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"
}
}
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export type { HawkStorage } from './storages/hawk-storage';
export type { UserManager } from './users/user-manager';
export { StorageUserManager } from './users/storage-user-manager';
49 changes: 49 additions & 0 deletions packages/core/src/users/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 StorageUserManager implements UserManager {
/**
* 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;
}

/** @inheritDoc */
public setUser(user: AffectedUser): void {
this.storage.setItem(HAWK_USER_STORAGE_KEY, user.id);
}

/** @inheritDoc */
public clear(): void {
this.storage.removeItem(HAWK_USER_STORAGE_KEY);
}
}
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/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 { StorageUserManager } from '../src';
import type { HawkStorage } from '../src';

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

beforeEach(() => {
storage = {
getItem: vi.fn().mockReturnValue(null),
setItem: vi.fn(),
removeItem: vi.fn(),
};
manager = new StorageUserManager(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'],
},
},
});
2 changes: 2 additions & 0 deletions packages/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
},
"homepage": "https://github.com/codex-team/hawk.javascript#readme",
"dependencies": {
"@hawk.so/browser": "workspace:^",
"@hawk.so/core": "workspace:^",
"error-stack-parser": "^2.1.4"
},
"devDependencies": {
Expand Down
61 changes: 27 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 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 @@ -19,6 +18,10 @@ import { isErrorProcessed, markErrorAsProcessed } from './utils/event';
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 { StorageUserManager } from '@hawk.so/core';
import { HawkLocalStorage } from '@hawk.so/browser';
import { id } from './utils/id';

/**
* Allow to use global VERSION, that will be overwritten by Webpack
Expand Down Expand Up @@ -62,11 +65,6 @@ export default class Catcher {
*/
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 @@ export default class Catcher {
*/
private readonly breadcrumbManager: BreadcrumbManager | null;

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

/**
* Catcher constructor
*
Expand All @@ -126,7 +129,9 @@ export default class Catcher {
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 @@ export default class Catcher {
}
}

/**
* 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();
localStorage.setItem(LOCAL_STORAGE_KEY, userId);
}

return {
id: userId,
};
}

/**
* Send test event from client
*/
Expand Down Expand Up @@ -272,14 +256,14 @@ export default class Catcher {
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 +549,19 @@ export default class Catcher {
}

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

this.userManager.setUser(newUser);

return newUser;
}

/**
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', '@hawk.so/browser'],
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'],
},
});
6 changes: 5 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ __metadata:
languageName: node
linkType: hard

"@hawk.so/browser@workspace:packages/browser":
"@hawk.so/browser@workspace:^, @hawk.so/browser@workspace:packages/browser":
version: 0.0.0-use.local
resolution: "@hawk.so/browser@workspace:packages/browser"
dependencies:
Expand All @@ -600,15 +600,19 @@ __metadata:
version: 0.0.0-use.local
resolution: "@hawk.so/core@workspace:packages/core"
dependencies:
"@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

"@hawk.so/javascript@npm:^3.0.0, @hawk.so/javascript@workspace:packages/javascript":
version: 0.0.0-use.local
resolution: "@hawk.so/javascript@workspace:packages/javascript"
dependencies:
"@hawk.so/browser": "workspace:^"
"@hawk.so/core": "workspace:^"
"@hawk.so/types": "npm:0.5.8"
error-stack-parser: "npm:^2.1.4"
jsdom: "npm:^28.0.0"
Expand Down