UserManager abstraction added#158
Conversation
37eecbd to
70760fd
Compare
09b7782 to
39d9d88
Compare
70760fd to
3f9a33a
Compare
a0132ff to
9a60f23
Compare
9e471fc to
2d655c1
Compare
9a60f23 to
eacefe1
Compare
2d655c1 to
c3013b6
Compare
c3013b6 to
223001d
Compare
eacefe1 to
7d5e8e4
Compare
| * {@link UserManager} implementation that persists the affected user | ||
| * in the browser's {@linkcode localStorage}. | ||
| */ | ||
| export class LocalStorageUserManager extends UserManager { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| * {@link UserManager} implementation that persists the affected user | ||
| * via an injected {@link HawkStorage} backend. | ||
| */ | ||
| export class HawkStorageUserManager implements UserManager { |
There was a problem hiding this comment.
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)
| if (storedId) { | ||
| userId = storedId; | ||
| } else { | ||
| userId = id(); |
There was a problem hiding this comment.
where we create the default id now?
There was a problem hiding this comment.
Pull request overview
This PR introduces a UserManager abstraction in @hawk.so/core to enable environment-agnostic user persistence, as part of the larger effort to extract shared logic from @hawk.so/javascript into a core package (related to issue #151).
Changes:
- Added
UserManagerinterface andHawkStorageUserManagerimplementation in@hawk.so/corefor environment-agnostic user management - Refactored
@hawk.so/javascriptto use the newUserManagerabstraction instead of directly accessing localStorage - Modified user initialization to be lazy (generated on first access) rather than eager (generated in constructor)
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/users/user-manager.ts | New interface defining the user management contract |
| packages/core/src/users/hawk-storage-user-manager.ts | Implementation of UserManager using HawkStorage for persistence |
| packages/core/src/index.ts | Exports new UserManager interface and implementation |
| packages/core/tests/users/hawk-storage-user-manager.test.ts | Unit tests for HawkStorageUserManager |
| packages/core/package.json | Added test scripts and dev dependencies for vitest |
| packages/core/vitest.config.ts | New vitest configuration for core package |
| packages/core/tsconfig.test.json | TypeScript config for test files |
| packages/javascript/src/catcher.ts | Refactored to use UserManager, removed getGeneratedUser, changed user initialization to lazy |
| packages/javascript/src/storages/hawk-local-storage.ts | Browser-specific implementation of HawkStorage |
| packages/javascript/package.json | Added @hawk.so/core as dependency |
| packages/javascript/vite.config.ts | Marked @hawk.so/core as external dependency |
| packages/javascript/vitest.config.ts | Added source condition for workspace dependency resolution |
| yarn.lock | Updated workspace dependency configurations |
| package.json | Added test:all and test:modified scripts for workspace testing |
| .github/workflows/main.yml | Updated test workflow to run tests on modified packages with fetch-depth: 0 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public getUser(): AffectedUser | null { | ||
| const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY); | ||
|
|
||
| if (storedId) { | ||
| return { | ||
| id: storedId, | ||
| }; | ||
| } | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
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.
| try { | ||
| const decodedIntegrationToken: DecodedIntegrationToken = JSON.parse(atob(this.token)); | ||
| const { integrationId } = decodedIntegrationToken; | ||
| const {integrationId} = decodedIntegrationToken; |
There was a problem hiding this comment.
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.
| const {integrationId} = decodedIntegrationToken; | |
| const { integrationId } = decodedIntegrationToken; |
| 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); | ||
| } |
There was a problem hiding this comment.
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).
| import { HawkStorageUserManager } from '../../src'; | ||
| import type { HawkStorage } from '../../src'; | ||
|
|
||
| describe('StorageUserManager', () => { |
There was a problem hiding this comment.
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.
| describe('StorageUserManager', () => { | |
| describe('HawkStorageUserManager', () => { |
| private getUser(): AffectedUser { | ||
| const user = this.userManager.getUser(); | ||
|
|
||
| if (user) { | ||
| return user; | ||
| } | ||
| const newUser: AffectedUser = {id: id()}; | ||
|
|
||
| this.userManager.setUser(newUser); | ||
|
|
||
| return newUser; | ||
| } |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| 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)); |
| */ | ||
| private getAddons(error: Error | string): HawkJavaScriptEvent['addons'] { | ||
| const { innerWidth, innerHeight } = window; | ||
| const {innerWidth, innerHeight} = window; |
There was a problem hiding this comment.
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.
| const {innerWidth, innerHeight} = window; | |
| const { innerWidth, innerHeight } = window; |
| const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY); | ||
|
|
||
| if (storedId) { | ||
| return { | ||
| id: storedId, | ||
| }; | ||
| } | ||
|
|
||
| return null; |
There was a problem hiding this comment.
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.
| 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; | |
| } |
| private getUser(): AffectedUser { | ||
| const user = this.userManager.getUser(); | ||
|
|
||
| if (user) { | ||
| return user; | ||
| } | ||
| const newUser: AffectedUser = {id: id()}; | ||
|
|
||
| this.userManager.setUser(newUser); | ||
|
|
||
| return newUser; | ||
| } |
There was a problem hiding this comment.
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.
| "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" | ||
| } |
There was a problem hiding this comment.
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.
8cbdac2 to
cd47f1e
Compare
Related #151
Added
UserManagerinterface which could be used as env-agnositc abstraction to persistAffectedUser's during catching errors.Also
StorageUserManagerimplementation based on env-agnosticHawkStorageadded.