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({ 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 ?? this.basename(path), this.readFileSync(path)); + } + + private resolvePath(key: string) { + 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 path; + } + + while (path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + + return path + '/'; + } +} 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'); + }); + }); +});