From dd9b2d5958d5970b6ebfc0c4ad28be801dc45fef Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Fri, 27 Jun 2025 13:18:27 +0200 Subject: [PATCH 1/3] browser: add simple implementation of BrowserFileSystem based on localStorage [INT-355] --- packages/browser/src/BacktraceClient.ts | 2 + .../browser/src/storage/BrowserFileSystem.ts | 90 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 packages/browser/src/storage/BrowserFileSystem.ts diff --git a/packages/browser/src/BacktraceClient.ts b/packages/browser/src/BacktraceClient.ts index 066aac85..cb3714c4 100644 --- a/packages/browser/src/BacktraceClient.ts +++ b/packages/browser/src/BacktraceClient.ts @@ -11,6 +11,7 @@ import { BacktraceConfiguration } from './BacktraceConfiguration.js'; import { BacktraceClientBuilder } from './builder/BacktraceClientBuilder.js'; import { BacktraceClientSetup } from './builder/BacktraceClientSetup.js'; import { getStackTraceConverter } from './converters/getStackTraceConverter.js'; +import { BrowserFileSystem } from './storage/BrowserFileSystem.js'; export class BacktraceClient extends BacktraceCoreClient { private readonly _disposeController: AbortController = new AbortController(); @@ -22,6 +23,7 @@ export class BacktraceClient { + return this.readDirSync(dir); + } + + public readDirSync(dir: string): string[] { + dir = this.resolvePath(this.ensureTrailingSlash(dir)); + + const result: string[] = []; + for (const key in this._storage) { + if (key.startsWith(dir)) { + result.push(key.substring(dir.length)); + } + } + + return result; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async createDir(_dir: string): Promise { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public createDirSync(_dir: string): void { + return; + } + + public async readFile(path: string): Promise { + return this.readFileSync(path); + } + + public readFileSync(path: string): string { + const result = this._storage.getItem(this.resolvePath(path)); + if (!result) { + throw new Error('path does not exist'); + } + return result; + } + + public async writeFile(path: string, content: string): Promise { + return this.writeFileSync(path, content); + } + + public writeFileSync(path: string, content: string): void { + this._storage.setItem(this.resolvePath(path), content); + } + + public async unlink(path: string): Promise { + return this.unlinkSync(path); + } + + public unlinkSync(path: string): void { + this._storage.removeItem(this.resolvePath(path)); + } + + public async exists(path: string): Promise { + return this.existsSync(path); + } + + public existsSync(path: string): boolean { + return this.resolvePath(path) in this._storage; + } + + public createAttachment(path: string, name?: string): BacktraceAttachment { + return new BacktraceStringAttachment(name ?? path, this.readFileSync(path)); + } + + private resolvePath(key: string) { + return PREFIX + key; + } + + private ensureTrailingSlash(path: string) { + if (path === '/') { + return '//'; + } + + while (path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + + return path + '/'; + } +} From 6f11ea011726a3224d0066ba07cf817a875908ea Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Fri, 27 Jun 2025 13:18:48 +0200 Subject: [PATCH 2/3] browser example: add database to browser example [INT-355] --- examples/sdk/browser/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/sdk/browser/src/index.ts b/examples/sdk/browser/src/index.ts index 58f6767a..f19b2e06 100644 --- a/examples/sdk/browser/src/index.ts +++ b/examples/sdk/browser/src/index.ts @@ -14,6 +14,10 @@ const client = BacktraceClient.builder({ prop2: 123, }, }, + database: { + enable: true, + path: '/', + }, }) .useModule( new BacktraceSessionReplayModule({ From bd6191eaefb3b8dcc80a7ee72f4bd37727c79212 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Wed, 9 Jul 2025 11:10:21 +0200 Subject: [PATCH 3/3] browser: add BrowserFileSystem tests [INT-355] --- .../browser/src/storage/BrowserFileSystem.ts | 23 ++- .../tests/storage/BrowserFileSystem.spec.ts | 174 ++++++++++++++++++ 2 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 packages/browser/tests/storage/BrowserFileSystem.spec.ts diff --git a/packages/browser/src/storage/BrowserFileSystem.ts b/packages/browser/src/storage/BrowserFileSystem.ts index 8a92c4a4..631a726f 100644 --- a/packages/browser/src/storage/BrowserFileSystem.ts +++ b/packages/browser/src/storage/BrowserFileSystem.ts @@ -69,16 +69,33 @@ export class BrowserFileSystem implements FileSystem { } public createAttachment(path: string, name?: string): BacktraceAttachment { - return new BacktraceStringAttachment(name ?? path, this.readFileSync(path)); + return new BacktraceStringAttachment(name ?? this.basename(path), this.readFileSync(path)); } private resolvePath(key: string) { - return PREFIX + key; + return PREFIX + this.ensureLeadingSlash(key); + } + + private basename(path: string) { + const lastSlashIndex = path.lastIndexOf('/'); + return lastSlashIndex === -1 ? path : path.substring(lastSlashIndex + 1); + } + + private ensureLeadingSlash(path: string) { + if (path === '/') { + return '/'; + } + + while (path.startsWith('/')) { + path = path.substring(1); + } + + return '/' + path; } private ensureTrailingSlash(path: string) { if (path === '/') { - return '//'; + return path; } while (path.endsWith('/')) { diff --git a/packages/browser/tests/storage/BrowserFileSystem.spec.ts b/packages/browser/tests/storage/BrowserFileSystem.spec.ts new file mode 100644 index 00000000..d75cdcc8 --- /dev/null +++ b/packages/browser/tests/storage/BrowserFileSystem.spec.ts @@ -0,0 +1,174 @@ +import { BrowserFileSystem } from '../../src/storage/BrowserFileSystem.js'; + +describe('BrowserFileSystem', () => { + beforeEach(() => { + localStorage.clear(); + }); + + describe('readDir', () => { + it('should return all values in path', () => { + localStorage.setItem('backtrace__/dir/1', 'test'); + localStorage.setItem('backtrace__/dir/2', 'test'); + + const fs = new BrowserFileSystem(localStorage); + const files = fs.readDirSync('dir'); + expect(files).toEqual(['1', '2']); + }); + + it('should return all values in absolute path', () => { + localStorage.setItem('backtrace__/dir1/dir2/1', 'test'); + localStorage.setItem('backtrace__/dir1/dir2/2', 'test'); + + const fs = new BrowserFileSystem(localStorage); + const files = fs.readDirSync('/dir1/dir2/'); + expect(files).toEqual(['1', '2']); + }); + + it('should return no values for non-existing keys', () => { + const fs = new BrowserFileSystem(localStorage); + const files = fs.readDirSync('/dir1/dir2/'); + expect(files).toEqual([]); + }); + + it('should not return values not prefixed by backtrace__', () => { + localStorage.setItem('test__/dir1/dir2/1', 'test'); + localStorage.setItem('backtrace__/dir1/dir2/2', 'test'); + + const fs = new BrowserFileSystem(localStorage); + const files = fs.readDirSync('/dir1/dir2/'); + expect(files).toEqual(['2']); + }); + }); + + describe('createDir', () => { + it('should do nothing', () => { + localStorage.setItem('backtrace__/dir1/dir2/1', 'test'); + + const fs = new BrowserFileSystem(localStorage); + fs.createDirSync('/a/b/c'); + + expect(Object.keys(localStorage)).toEqual(['backtrace__/dir1/dir2/1']); + }); + }); + + describe('readFile', () => { + it('should return key contents', () => { + localStorage.setItem('backtrace__/dir1/dir2/1', 'test'); + + const fs = new BrowserFileSystem(localStorage); + const actual = fs.readFileSync('/dir1/dir2/1'); + + expect(actual).toEqual('test'); + }); + + it('should throw if key does not exist', () => { + const fs = new BrowserFileSystem(localStorage); + expect(() => fs.readFileSync('/dir1/dir2/1')).toThrow('path does not exist'); + }); + }); + + describe('writeFile', () => { + it('should write key contents', () => { + const fs = new BrowserFileSystem(localStorage); + fs.writeFileSync('/dir1/dir2/1', 'test'); + + expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toEqual('test'); + }); + + it('should write key contents with relative path', () => { + const fs = new BrowserFileSystem(localStorage); + fs.writeFileSync('dir1/dir2/1', 'test'); + + expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toEqual('test'); + }); + }); + + describe('unlink', () => { + it('should remove file from storage', () => { + localStorage.setItem('backtrace__/dir1/dir2/1', 'test'); + + const fs = new BrowserFileSystem(localStorage); + fs.unlinkSync('/dir1/dir2/1'); + + expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toBeNull(); + }); + + it('should remove file from storage with relative path', () => { + localStorage.setItem('backtrace__/dir1/dir2/1', 'test'); + + const fs = new BrowserFileSystem(localStorage); + fs.unlinkSync('dir1/dir2/1'); + + expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toBeNull(); + }); + + it('should not throw if file does not exist', () => { + const fs = new BrowserFileSystem(localStorage); + expect(() => fs.unlinkSync('/dir1/dir2/1')).not.toThrow(); + }); + }); + + describe('existsSync', () => { + it('should return true if file exists', () => { + localStorage.setItem('backtrace__/dir1/dir2/1', 'test'); + + const fs = new BrowserFileSystem(localStorage); + const exists = fs.existsSync('/dir1/dir2/1'); + + expect(exists).toBe(true); + }); + + it('should return true if file exists wit relative path', () => { + localStorage.setItem('backtrace__/dir1/dir2/1', 'test'); + + const fs = new BrowserFileSystem(localStorage); + const exists = fs.existsSync('dir1/dir2/1'); + + expect(exists).toBe(true); + }); + + it('should return false if file does not exist', () => { + const fs = new BrowserFileSystem(localStorage); + const exists = fs.existsSync('/dir1/dir2/1'); + + expect(exists).toBe(false); + }); + }); + + describe('createAttachment', () => { + it('should create attachment from file contents', () => { + localStorage.setItem('backtrace__/path/to/file.txt', 'file content'); + + const fs = new BrowserFileSystem(localStorage); + const attachment = fs.createAttachment('/path/to/file.txt'); + + expect(attachment.name).toBe('file.txt'); + expect(attachment.get()).toBe('file content'); + }); + + it('should create attachment from file contents with relative path', () => { + localStorage.setItem('backtrace__/path/to/file.txt', 'file content'); + + const fs = new BrowserFileSystem(localStorage); + const attachment = fs.createAttachment('path/to/file.txt'); + + expect(attachment.name).toBe('file.txt'); + expect(attachment.get()).toBe('file content'); + }); + + it('should create attachment with custom name', () => { + localStorage.setItem('backtrace__/path/to/file.txt', 'file content'); + + const fs = new BrowserFileSystem(localStorage); + const attachment = fs.createAttachment('/path/to/file.txt', 'custom-name.txt'); + + expect(attachment.name).toBe('custom-name.txt'); + expect(attachment.get()).toBe('file content'); + }); + + it('should throw if file does not exist', () => { + const fs = new BrowserFileSystem(localStorage); + expect(() => fs.createAttachment('/nonexistent/file.txt')).toThrow('path does not exist'); + }); + }); +});