diff --git a/package.json b/package.json index f40f657..01c1ec1 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "version": "0.0.0", "packageManager": "yarn@4.12.0", "workspaces": [ + "packages/core", "packages/javascript", "packages/sveltekit", "packages/sveltekit/playground" diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..1c4cc2f --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,40 @@ +{ + "name": "@hawk.so/core", + "version": "1.0.0", + "description": "Base implementation for all Hawk.so SDKs", + "files": [ + "dist" + ], + "main": "./dist/hawk-core.umd.js", + "module": "./dist/hawk-core.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/hawk-core.mjs", + "require": "./dist/hawk-core.umd.js" + } + }, + "scripts": { + "build": "vite build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/codex-team/hawk.javascript.git", + "directory": "packages/core" + }, + "author": { + "name": "CodeX", + "email": "team@codex.so" + }, + "license": "AGPL-3.0-only", + "bugs": { + "url": "https://github.com/codex-team/hawk.javascript/issues" + }, + "homepage": "https://github.com/codex-team/hawk.javascript#readme", + "devDependencies": { + "vite": "^7.3.1", + "vite-plugin-dts": "^4.2.4" + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..fb7db31 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,3 @@ +export type { HawkStorage } from './types/storage'; +export type { UserManager } from './types/user-manager'; +export { StorageUserManager } from './types/storage-user-manager'; diff --git a/packages/core/src/types/storage-user-manager.ts b/packages/core/src/types/storage-user-manager.ts new file mode 100644 index 0000000..76117d6 --- /dev/null +++ b/packages/core/src/types/storage-user-manager.ts @@ -0,0 +1,63 @@ +import { AffectedUser } from "@hawk.so/types"; +import { id } from "../utils/id"; +import { HawkStorage } from "./storage"; +import { 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; + } + + /** + * Returns the stored user if one exists. Otherwise, generates a new identifier, + * saves it in storage under {@linkcode HAWK_USER_STORAGE_KEY}, and returns the new user. + */ + getUser(): AffectedUser { + const storedId = this.storage.getItem(HAWK_USER_STORAGE_KEY); + if (storedId) { + return { + id: storedId, + }; + } + + const userId = id() + this.storage.setItem(HAWK_USER_STORAGE_KEY, userId); + return { + id: userId + }; + } + + /** + * Persists the given user's identifier in storage. + * + * @param user - The affected user to store. + */ + setUser(user: AffectedUser): void { + this.storage.setItem(HAWK_USER_STORAGE_KEY, user.id); + } + + /** + * Removes the stored user identifier from storage. + */ + clear(): void { + this.storage.removeItem(HAWK_USER_STORAGE_KEY) + } +} diff --git a/packages/core/src/types/storage.ts b/packages/core/src/types/storage.ts new file mode 100644 index 0000000..40bd694 --- /dev/null +++ b/packages/core/src/types/storage.ts @@ -0,0 +1,27 @@ +/** + * Abstract key–value storage contract used by Hawk internals + * (e.g. {@link StorageUserManager}) 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 +} diff --git a/packages/core/src/types/user-manager.ts b/packages/core/src/types/user-manager.ts new file mode 100644 index 0000000..050100f --- /dev/null +++ b/packages/core/src/types/user-manager.ts @@ -0,0 +1,26 @@ +import { 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, creating one if none exists yet. + */ + getUser(): AffectedUser + + /** + * 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 +} diff --git a/packages/javascript/src/utils/id.ts b/packages/core/src/utils/id.ts similarity index 100% rename from packages/javascript/src/utils/id.ts rename to packages/core/src/utils/id.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..56896f0 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts new file mode 100644 index 0000000..2d03290 --- /dev/null +++ b/packages/core/vite.config.ts @@ -0,0 +1,22 @@ +import path from 'path'; +import dts from 'vite-plugin-dts'; +import { defineConfig } from 'vite'; + +export default defineConfig(() => { + return { + build: { + copyPublicDir: false, + lib: { + entry: path.resolve(__dirname, 'src', 'index.ts'), + name: 'HawkCore', + fileName: 'hawk-core', + }, + }, + + plugins: [ + dts({ + tsconfigPath: './tsconfig.json', + }), + ], + }; +}); diff --git a/packages/javascript/package.json b/packages/javascript/package.json index 22e9827..d8eadf9 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -39,6 +39,7 @@ }, "homepage": "https://github.com/codex-team/hawk.javascript#readme", "dependencies": { + "@hawk.so/core": "workspace:^", "error-stack-parser": "^2.1.4" }, "devDependencies": { diff --git a/packages/javascript/src/catcher.ts b/packages/javascript/src/catcher.ts index b18d868..0286bfb 100644 --- a/packages/javascript/src/catcher.ts +++ b/packages/javascript/src/catcher.ts @@ -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, @@ -19,6 +18,8 @@ import { isErrorProcessed, markErrorAsProcessed } from './utils/event'; import { ConsoleCatcher } from './addons/consoleCatcher'; import { BreadcrumbManager } from './addons/breadcrumbs'; import { validateUser, validateContext, isValidEventPayload } from './utils/validation'; +import { StorageUserManager, UserManager } from "@hawk.so/core"; +import { HawkLocalStorage } from "./modules/local-storage"; /** * Allow to use global VERSION, that will be overwritten by Webpack @@ -62,11 +63,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 */ @@ -111,6 +107,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 * @@ -126,7 +127,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 = @@ -189,27 +192,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 */ @@ -272,14 +254,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() } /** @@ -533,7 +515,7 @@ export default class Catcher { private getIntegrationId(): string { try { const decodedIntegrationToken: DecodedIntegrationToken = JSON.parse(atob(this.token)); - const { integrationId } = decodedIntegrationToken; + const {integrationId} = decodedIntegrationToken; if (!integrationId || integrationId === '') { throw new Error(); @@ -568,7 +550,7 @@ export default class Catcher { * Current authenticated user */ private getUser(): HawkJavaScriptEvent['user'] { - return this.user || null; + return this.userManager.getUser(); } /** diff --git a/packages/javascript/src/modules/local-storage.ts b/packages/javascript/src/modules/local-storage.ts new file mode 100644 index 0000000..d7f0f1c --- /dev/null +++ b/packages/javascript/src/modules/local-storage.ts @@ -0,0 +1,21 @@ +import { HawkStorage } from "@hawk.so/core"; + +/** + * {@link HawkStorage} implementation backed by the browser's {@linkcode localStorage}. + */ +export class HawkLocalStorage implements HawkStorage { + /** @inheritDoc */ + public getItem(key: string): string | null { + return localStorage.getItem(key); + } + + /** @inheritDoc */ + public setItem(key: string, value: string): void { + localStorage.setItem(key, value); + } + + /** @inheritDoc */ + public removeItem(key: string): void { + localStorage.removeItem(key); + } +} diff --git a/packages/javascript/vite.config.ts b/packages/javascript/vite.config.ts index 47fe52e..65b622e 100644 --- a/packages/javascript/vite.config.ts +++ b/packages/javascript/vite.config.ts @@ -26,6 +26,7 @@ export default defineConfig(() => { fileName: 'hawk', }, rollupOptions: { + external: ['@hawk.so/core'], plugins: [ license({ thirdParty: { diff --git a/yarn.lock b/yarn.lock index 317217e..fed6aff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -576,10 +576,20 @@ __metadata: languageName: node linkType: hard +"@hawk.so/core@workspace:^, @hawk.so/core@workspace:packages/core": + version: 0.0.0-use.local + resolution: "@hawk.so/core@workspace:packages/core" + dependencies: + vite: "npm:^7.3.1" + vite-plugin-dts: "npm:^4.2.4" + 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/core": "workspace:^" "@hawk.so/types": "npm:0.5.8" error-stack-parser: "npm:^2.1.4" jsdom: "npm:^28.0.0"