Skip to content

Commit ac6e4b0

Browse files
authored
browser: add browser file system based on localStorage (#348)
* browser: add simple implementation of BrowserFileSystem based on localStorage [INT-355] * browser example: add database to browser example [INT-355] * browser: add BrowserFileSystem tests [INT-355]
1 parent a7eb2c9 commit ac6e4b0

File tree

4 files changed

+287
-0
lines changed

4 files changed

+287
-0
lines changed

examples/sdk/browser/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ const client = BacktraceClient.builder({
1414
prop2: 123,
1515
},
1616
},
17+
database: {
18+
enable: true,
19+
path: '/',
20+
},
1721
})
1822
.useModule(
1923
new BacktraceSessionReplayModule({

packages/browser/src/BacktraceClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { BacktraceConfiguration } from './BacktraceConfiguration.js';
1111
import { BacktraceClientBuilder } from './builder/BacktraceClientBuilder.js';
1212
import { BacktraceClientSetup } from './builder/BacktraceClientSetup.js';
1313
import { getStackTraceConverter } from './converters/getStackTraceConverter.js';
14+
import { BrowserFileSystem } from './storage/BrowserFileSystem.js';
1415

1516
export class BacktraceClient<O extends BacktraceConfiguration = BacktraceConfiguration> extends BacktraceCoreClient<O> {
1617
private readonly _disposeController: AbortController = new AbortController();
@@ -22,6 +23,7 @@ export class BacktraceClient<O extends BacktraceConfiguration = BacktraceConfigu
2223
requestHandler: new BacktraceBrowserRequestHandler(clientSetup.options),
2324
debugIdMapProvider: new VariableDebugIdMapProvider(window as DebugIdContainer),
2425
sessionProvider: new BacktraceBrowserSessionProvider(),
26+
fileSystem: new BrowserFileSystem(),
2527
...clientSetup,
2628
});
2729

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { BacktraceAttachment, BacktraceStringAttachment, FileSystem } from '@backtrace/sdk-core';
2+
3+
const PREFIX = 'backtrace__';
4+
5+
export class BrowserFileSystem implements FileSystem {
6+
constructor(private readonly _storage = window.localStorage) {}
7+
8+
public async readDir(dir: string): Promise<string[]> {
9+
return this.readDirSync(dir);
10+
}
11+
12+
public readDirSync(dir: string): string[] {
13+
dir = this.resolvePath(this.ensureTrailingSlash(dir));
14+
15+
const result: string[] = [];
16+
for (const key in this._storage) {
17+
if (key.startsWith(dir)) {
18+
result.push(key.substring(dir.length));
19+
}
20+
}
21+
22+
return result;
23+
}
24+
25+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26+
public async createDir(_dir: string): Promise<void> {
27+
return;
28+
}
29+
30+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
31+
public createDirSync(_dir: string): void {
32+
return;
33+
}
34+
35+
public async readFile(path: string): Promise<string> {
36+
return this.readFileSync(path);
37+
}
38+
39+
public readFileSync(path: string): string {
40+
const result = this._storage.getItem(this.resolvePath(path));
41+
if (!result) {
42+
throw new Error('path does not exist');
43+
}
44+
return result;
45+
}
46+
47+
public async writeFile(path: string, content: string): Promise<void> {
48+
return this.writeFileSync(path, content);
49+
}
50+
51+
public writeFileSync(path: string, content: string): void {
52+
this._storage.setItem(this.resolvePath(path), content);
53+
}
54+
55+
public async unlink(path: string): Promise<void> {
56+
return this.unlinkSync(path);
57+
}
58+
59+
public unlinkSync(path: string): void {
60+
this._storage.removeItem(this.resolvePath(path));
61+
}
62+
63+
public async exists(path: string): Promise<boolean> {
64+
return this.existsSync(path);
65+
}
66+
67+
public existsSync(path: string): boolean {
68+
return this.resolvePath(path) in this._storage;
69+
}
70+
71+
public createAttachment(path: string, name?: string): BacktraceAttachment {
72+
return new BacktraceStringAttachment(name ?? this.basename(path), this.readFileSync(path));
73+
}
74+
75+
private resolvePath(key: string) {
76+
return PREFIX + this.ensureLeadingSlash(key);
77+
}
78+
79+
private basename(path: string) {
80+
const lastSlashIndex = path.lastIndexOf('/');
81+
return lastSlashIndex === -1 ? path : path.substring(lastSlashIndex + 1);
82+
}
83+
84+
private ensureLeadingSlash(path: string) {
85+
if (path === '/') {
86+
return '/';
87+
}
88+
89+
while (path.startsWith('/')) {
90+
path = path.substring(1);
91+
}
92+
93+
return '/' + path;
94+
}
95+
96+
private ensureTrailingSlash(path: string) {
97+
if (path === '/') {
98+
return path;
99+
}
100+
101+
while (path.endsWith('/')) {
102+
path = path.substring(0, path.length - 1);
103+
}
104+
105+
return path + '/';
106+
}
107+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { BrowserFileSystem } from '../../src/storage/BrowserFileSystem.js';
2+
3+
describe('BrowserFileSystem', () => {
4+
beforeEach(() => {
5+
localStorage.clear();
6+
});
7+
8+
describe('readDir', () => {
9+
it('should return all values in path', () => {
10+
localStorage.setItem('backtrace__/dir/1', 'test');
11+
localStorage.setItem('backtrace__/dir/2', 'test');
12+
13+
const fs = new BrowserFileSystem(localStorage);
14+
const files = fs.readDirSync('dir');
15+
expect(files).toEqual(['1', '2']);
16+
});
17+
18+
it('should return all values in absolute path', () => {
19+
localStorage.setItem('backtrace__/dir1/dir2/1', 'test');
20+
localStorage.setItem('backtrace__/dir1/dir2/2', 'test');
21+
22+
const fs = new BrowserFileSystem(localStorage);
23+
const files = fs.readDirSync('/dir1/dir2/');
24+
expect(files).toEqual(['1', '2']);
25+
});
26+
27+
it('should return no values for non-existing keys', () => {
28+
const fs = new BrowserFileSystem(localStorage);
29+
const files = fs.readDirSync('/dir1/dir2/');
30+
expect(files).toEqual([]);
31+
});
32+
33+
it('should not return values not prefixed by backtrace__', () => {
34+
localStorage.setItem('test__/dir1/dir2/1', 'test');
35+
localStorage.setItem('backtrace__/dir1/dir2/2', 'test');
36+
37+
const fs = new BrowserFileSystem(localStorage);
38+
const files = fs.readDirSync('/dir1/dir2/');
39+
expect(files).toEqual(['2']);
40+
});
41+
});
42+
43+
describe('createDir', () => {
44+
it('should do nothing', () => {
45+
localStorage.setItem('backtrace__/dir1/dir2/1', 'test');
46+
47+
const fs = new BrowserFileSystem(localStorage);
48+
fs.createDirSync('/a/b/c');
49+
50+
expect(Object.keys(localStorage)).toEqual(['backtrace__/dir1/dir2/1']);
51+
});
52+
});
53+
54+
describe('readFile', () => {
55+
it('should return key contents', () => {
56+
localStorage.setItem('backtrace__/dir1/dir2/1', 'test');
57+
58+
const fs = new BrowserFileSystem(localStorage);
59+
const actual = fs.readFileSync('/dir1/dir2/1');
60+
61+
expect(actual).toEqual('test');
62+
});
63+
64+
it('should throw if key does not exist', () => {
65+
const fs = new BrowserFileSystem(localStorage);
66+
expect(() => fs.readFileSync('/dir1/dir2/1')).toThrow('path does not exist');
67+
});
68+
});
69+
70+
describe('writeFile', () => {
71+
it('should write key contents', () => {
72+
const fs = new BrowserFileSystem(localStorage);
73+
fs.writeFileSync('/dir1/dir2/1', 'test');
74+
75+
expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toEqual('test');
76+
});
77+
78+
it('should write key contents with relative path', () => {
79+
const fs = new BrowserFileSystem(localStorage);
80+
fs.writeFileSync('dir1/dir2/1', 'test');
81+
82+
expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toEqual('test');
83+
});
84+
});
85+
86+
describe('unlink', () => {
87+
it('should remove file from storage', () => {
88+
localStorage.setItem('backtrace__/dir1/dir2/1', 'test');
89+
90+
const fs = new BrowserFileSystem(localStorage);
91+
fs.unlinkSync('/dir1/dir2/1');
92+
93+
expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toBeNull();
94+
});
95+
96+
it('should remove file from storage with relative path', () => {
97+
localStorage.setItem('backtrace__/dir1/dir2/1', 'test');
98+
99+
const fs = new BrowserFileSystem(localStorage);
100+
fs.unlinkSync('dir1/dir2/1');
101+
102+
expect(localStorage.getItem('backtrace__/dir1/dir2/1')).toBeNull();
103+
});
104+
105+
it('should not throw if file does not exist', () => {
106+
const fs = new BrowserFileSystem(localStorage);
107+
expect(() => fs.unlinkSync('/dir1/dir2/1')).not.toThrow();
108+
});
109+
});
110+
111+
describe('existsSync', () => {
112+
it('should return true if file exists', () => {
113+
localStorage.setItem('backtrace__/dir1/dir2/1', 'test');
114+
115+
const fs = new BrowserFileSystem(localStorage);
116+
const exists = fs.existsSync('/dir1/dir2/1');
117+
118+
expect(exists).toBe(true);
119+
});
120+
121+
it('should return true if file exists wit relative path', () => {
122+
localStorage.setItem('backtrace__/dir1/dir2/1', 'test');
123+
124+
const fs = new BrowserFileSystem(localStorage);
125+
const exists = fs.existsSync('dir1/dir2/1');
126+
127+
expect(exists).toBe(true);
128+
});
129+
130+
it('should return false if file does not exist', () => {
131+
const fs = new BrowserFileSystem(localStorage);
132+
const exists = fs.existsSync('/dir1/dir2/1');
133+
134+
expect(exists).toBe(false);
135+
});
136+
});
137+
138+
describe('createAttachment', () => {
139+
it('should create attachment from file contents', () => {
140+
localStorage.setItem('backtrace__/path/to/file.txt', 'file content');
141+
142+
const fs = new BrowserFileSystem(localStorage);
143+
const attachment = fs.createAttachment('/path/to/file.txt');
144+
145+
expect(attachment.name).toBe('file.txt');
146+
expect(attachment.get()).toBe('file content');
147+
});
148+
149+
it('should create attachment from file contents with relative path', () => {
150+
localStorage.setItem('backtrace__/path/to/file.txt', 'file content');
151+
152+
const fs = new BrowserFileSystem(localStorage);
153+
const attachment = fs.createAttachment('path/to/file.txt');
154+
155+
expect(attachment.name).toBe('file.txt');
156+
expect(attachment.get()).toBe('file content');
157+
});
158+
159+
it('should create attachment with custom name', () => {
160+
localStorage.setItem('backtrace__/path/to/file.txt', 'file content');
161+
162+
const fs = new BrowserFileSystem(localStorage);
163+
const attachment = fs.createAttachment('/path/to/file.txt', 'custom-name.txt');
164+
165+
expect(attachment.name).toBe('custom-name.txt');
166+
expect(attachment.get()).toBe('file content');
167+
});
168+
169+
it('should throw if file does not exist', () => {
170+
const fs = new BrowserFileSystem(localStorage);
171+
expect(() => fs.createAttachment('/nonexistent/file.txt')).toThrow('path does not exist');
172+
});
173+
});
174+
});

0 commit comments

Comments
 (0)