Skip to content

Commit ed0b418

Browse files
committed
node: overhaul of database and storage [INT-355]
1 parent d7359a3 commit ed0b418

24 files changed

+584
-188
lines changed

packages/node/src/BacktraceClient.ts

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
SessionFiles,
88
VariableDebugIdMapProvider,
99
} from '@backtrace/sdk-core';
10-
import path from 'path';
10+
import nodeFs from 'fs';
1111
import { BacktraceConfiguration, BacktraceSetupConfiguration } from './BacktraceConfiguration.js';
1212
import { BacktraceNodeRequestHandler } from './BacktraceNodeRequestHandler.js';
1313
import { AGENT } from './agentDefinition.js';
@@ -17,39 +17,85 @@ import { FileBreadcrumbsStorage } from './breadcrumbs/FileBreadcrumbsStorage.js'
1717
import { BacktraceClientBuilder } from './builder/BacktraceClientBuilder.js';
1818
import { BacktraceNodeClientSetup } from './builder/BacktraceClientSetup.js';
1919
import { NodeOptionReader } from './common/NodeOptionReader.js';
20+
import { toArray } from './common/asyncGenerator.js';
2021
import { NodeDiagnosticReportConverter } from './converter/NodeDiagnosticReportConverter.js';
21-
import { FsNodeFileSystem } from './storage/FsNodeFileSystem.js';
22-
import { NodeFileSystem } from './storage/interfaces/NodeFileSystem.js';
22+
import {
23+
AttachmentBacktraceDatabaseRecordSender,
24+
AttachmentBacktraceDatabaseRecordSerializer,
25+
} from './database/AttachmentBacktraceDatabaseRecord.js';
26+
import {
27+
ReportBacktraceDatabaseRecordWithAttachmentsFactory,
28+
ReportBacktraceDatabaseRecordWithAttachmentsSender,
29+
ReportBacktraceDatabaseRecordWithAttachmentsSerializer,
30+
} from './database/ReportBacktraceDatabaseRecordWithAttachments.js';
31+
import { assertDatabasePath } from './database/utils.js';
32+
import { BacktraceStorageModule } from './storage/BacktraceStorage.js';
33+
import { BacktraceStorageModuleFactory } from './storage/BacktraceStorageModuleFactory.js';
34+
import { NodeFsBacktraceStorageModuleFactory } from './storage/NodeFsBacktraceStorage.js';
2335

2436
export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration> {
2537
private _listeners: Record<string, NodeJS.UnhandledRejectionListener | NodeJS.UncaughtExceptionListener> = {};
2638

27-
protected get nodeFileSystem() {
28-
return this.fileSystem as NodeFileSystem | undefined;
39+
protected readonly storageFactory: BacktraceStorageModuleFactory;
40+
protected readonly fs: typeof nodeFs;
41+
42+
protected get databaseNodeFsStorage() {
43+
return this.databaseStorage as BacktraceStorageModule | undefined;
2944
}
3045

3146
constructor(clientSetup: BacktraceNodeClientSetup) {
32-
const fileSystem = clientSetup.fileSystem ?? new FsNodeFileSystem();
47+
const storageFactory = clientSetup.storageFactory ?? new NodeFsBacktraceStorageModuleFactory();
48+
const fs = clientSetup.fs ?? nodeFs;
49+
const storage =
50+
clientSetup.database?.storage ??
51+
(clientSetup.options.database?.enable
52+
? storageFactory.create({
53+
path: assertDatabasePath(clientSetup.options.database.path),
54+
createDirectory: clientSetup.options.database.createDatabaseDirectory,
55+
fs,
56+
})
57+
: undefined);
58+
3359
super({
3460
sdkOptions: AGENT,
3561
requestHandler: new BacktraceNodeRequestHandler(clientSetup.options),
3662
debugIdMapProvider: new VariableDebugIdMapProvider(global as DebugIdContainer),
63+
database:
64+
clientSetup.options.database?.enable && storage
65+
? {
66+
storage,
67+
reportRecordFactory: ReportBacktraceDatabaseRecordWithAttachmentsFactory.default(),
68+
...clientSetup.database,
69+
recordSenders: (submission) => ({
70+
report: new ReportBacktraceDatabaseRecordWithAttachmentsSender(submission),
71+
attachment: new AttachmentBacktraceDatabaseRecordSender(submission),
72+
...clientSetup.database?.recordSenders?.(submission),
73+
}),
74+
recordSerializers: {
75+
report: new ReportBacktraceDatabaseRecordWithAttachmentsSerializer(storage),
76+
attachment: new AttachmentBacktraceDatabaseRecordSerializer(fs),
77+
...clientSetup.database?.recordSerializers,
78+
},
79+
}
80+
: undefined,
3781
...clientSetup,
38-
fileSystem,
3982
options: {
4083
...clientSetup.options,
4184
attachments: clientSetup.options.attachments?.map(transformAttachment),
4285
},
4386
});
4487

88+
this.storageFactory = storageFactory;
89+
this.fs = fs;
90+
4591
const breadcrumbsManager = this.modules.get(BreadcrumbsManager);
46-
if (breadcrumbsManager && this.sessionFiles) {
47-
breadcrumbsManager.setStorage(FileBreadcrumbsStorage.factory(this.sessionFiles, fileSystem));
92+
if (breadcrumbsManager && this.sessionFiles && storage) {
93+
breadcrumbsManager.setStorage(FileBreadcrumbsStorage.factory(this.sessionFiles, storage));
4894
}
4995

50-
if (this.sessionFiles && clientSetup.options.database?.captureNativeCrashes) {
51-
this.addModule(FileAttributeManager, FileAttributeManager.create(fileSystem));
52-
this.addModule(FileAttachmentsManager, FileAttachmentsManager.create(fileSystem));
96+
if (this.sessionFiles && storage && clientSetup.options.database?.captureNativeCrashes) {
97+
this.addModule(FileAttributeManager, FileAttributeManager.create(storage));
98+
this.addModule(FileAttachmentsManager, FileAttachmentsManager.create(storage));
5399
}
54100
}
55101

@@ -58,6 +104,7 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>
58104

59105
try {
60106
super.initialize();
107+
61108
this.captureUnhandledErrors(
62109
this.options.captureUnhandledErrors,
63110
this.options.captureUnhandledPromiseRejections,
@@ -242,18 +289,19 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>
242289
}
243290

244291
private async loadNodeCrashes() {
245-
if (!this.database || !this.nodeFileSystem || !this.options.database?.captureNativeCrashes) {
292+
if (!this.database || !this.options.database?.captureNativeCrashes) {
246293
return;
247294
}
248295

249296
const reportName = process.report?.filename;
250-
const databasePath = process.report?.directory
251-
? process.report.directory
252-
: (this.options.database?.path ?? process.cwd());
297+
const storage = this.storageFactory.create({
298+
path: process.report?.directory ? process.report.directory : (this.options.database?.path ?? process.cwd()),
299+
fs: this.fs,
300+
});
253301

254302
let databaseFiles: string[];
255303
try {
256-
databaseFiles = await this.nodeFileSystem.readDir(databasePath);
304+
databaseFiles = await toArray(storage.keys());
257305
} catch {
258306
return;
259307
}
@@ -271,11 +319,14 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>
271319

272320
const reports: [path: string, report: BacktraceReport, sessionFiles?: SessionFiles][] = [];
273321
for (const recordName of recordNames) {
274-
const recordPath = path.join(databasePath, recordName);
275322
try {
276-
const recordJson = await this.nodeFileSystem.readFile(recordPath);
323+
const recordJson = await storage.get(recordName);
324+
if (!recordJson) {
325+
continue;
326+
}
327+
277328
const report = converter.convert(JSON.parse(recordJson));
278-
reports.push([recordPath, report]);
329+
reports.push([recordName, report]);
279330
} catch {
280331
// Do nothing, skip the report
281332
}
@@ -292,17 +343,15 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>
292343
currentSession = currentSession?.getPreviousSession();
293344
}
294345

295-
for (const [recordPath, report, session] of reports) {
346+
for (const [recordName, report, session] of reports) {
296347
try {
297348
if (session) {
298-
report.attachments.push(
299-
...FileBreadcrumbsStorage.getSessionAttachments(session, this.nodeFileSystem),
300-
);
349+
report.attachments.push(...FileBreadcrumbsStorage.getSessionAttachments(session, storage));
301350

302-
const fileAttributes = FileAttributeManager.createFromSession(session, this.nodeFileSystem);
351+
const fileAttributes = FileAttributeManager.createFromSession(session, storage);
303352
Object.assign(report.attributes, await fileAttributes.get());
304353

305-
const fileAttachments = FileAttachmentsManager.createFromSession(session, this.nodeFileSystem);
354+
const fileAttachments = FileAttachmentsManager.createFromSession(session, storage);
306355
report.attachments.push(...(await fileAttachments.get()));
307356

308357
report.attributes['application.session'] = session.sessionId;
@@ -318,7 +367,7 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>
318367
// Do nothing, skip the report
319368
} finally {
320369
try {
321-
await this.nodeFileSystem.unlink(recordPath);
370+
await storage.remove(recordName);
322371
} catch {
323372
// Do nothing
324373
}
Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,37 @@
1-
import { BacktraceAttachment, BacktraceConfiguration as CoreConfiguration } from '@backtrace/sdk-core';
1+
import {
2+
BacktraceAttachment,
3+
BacktraceConfiguration as CoreConfiguration,
4+
DisabledBacktraceDatabaseConfiguration as CoreDisabledBacktraceDatabaseConfiguration,
5+
EnabledBacktraceDatabaseConfiguration as CoreEnabledBacktraceDatabaseConfiguration,
6+
} from '@backtrace/sdk-core';
27
import { Readable } from 'stream';
38

4-
export interface BacktraceSetupConfiguration extends Omit<CoreConfiguration, 'attachments'> {
9+
export interface EnabledBacktraceDatabaseConfiguration extends CoreEnabledBacktraceDatabaseConfiguration {
10+
/**
11+
* Path where the SDK can store data.
12+
*/
13+
path: string;
14+
/**
15+
* Determine if the directory should be auto created by the SDK.
16+
* @default true
17+
*/
18+
createDatabaseDirectory?: boolean;
19+
}
20+
21+
export interface DisabledBacktraceDatabaseConfiguration
22+
extends CoreDisabledBacktraceDatabaseConfiguration,
23+
Omit<Partial<EnabledBacktraceDatabaseConfiguration>, 'enable'> {}
24+
25+
export type BacktraceDatabaseConfiguration =
26+
| EnabledBacktraceDatabaseConfiguration
27+
| DisabledBacktraceDatabaseConfiguration;
28+
29+
export interface BacktraceSetupConfiguration extends Omit<CoreConfiguration, 'attachments' | 'database'> {
530
attachments?: Array<BacktraceAttachment<Buffer | Readable | string | Uint8Array> | string>;
31+
database?: BacktraceDatabaseConfiguration;
632
}
733

8-
export interface BacktraceConfiguration extends Omit<CoreConfiguration, 'attachments'> {
34+
export interface BacktraceConfiguration extends Omit<CoreConfiguration, 'attachments' | 'database'> {
935
attachments?: BacktraceAttachment<Buffer | Readable | string | Uint8Array>[];
36+
database?: BacktraceDatabaseConfiguration;
1037
}
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
import { BacktraceFileAttachment as CoreBacktraceFileAttachment } from '@backtrace/sdk-core';
1+
import { BacktraceSyncStorage, BacktraceFileAttachment as CoreBacktraceFileAttachment } from '@backtrace/sdk-core';
22
import fs from 'fs';
33
import path from 'path';
44
import { Readable } from 'stream';
5-
import { NodeFileSystem } from '../storage/interfaces/NodeFileSystem.js';
5+
import { BacktraceStreamStorage } from '../storage/BacktraceStorage.js';
66

77
export class BacktraceFileAttachment implements CoreBacktraceFileAttachment<Readable> {
88
public readonly name: string;
99

1010
constructor(
1111
public readonly filePath: string,
1212
name?: string,
13-
private readonly _fileSystem?: NodeFileSystem,
13+
private readonly _fs: typeof fs | (BacktraceSyncStorage & BacktraceStreamStorage) = fs,
1414
) {
1515
this.name = name ?? path.basename(this.filePath);
1616
}
1717

1818
public get(): Readable | undefined {
19-
if (!(this._fileSystem ?? fs).existsSync(this.filePath)) {
20-
return undefined;
19+
if ('hasSync' in this._fs) {
20+
if (!this._fs.hasSync(this.filePath)) {
21+
return undefined;
22+
}
23+
} else {
24+
if (!this._fs.existsSync(this.filePath)) {
25+
return undefined;
26+
}
2127
}
22-
return (this._fileSystem ?? fs).createReadStream(this.filePath);
28+
29+
return this._fs.createReadStream(this.filePath);
2330
}
2431
}

packages/node/src/attachment/FileAttachmentsManager.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
AttachmentManager,
33
BacktraceModule,
44
BacktraceModuleBindData,
5-
FileSystem,
5+
BacktraceStorage,
66
SessionFiles,
77
} from '@backtrace/sdk-core';
88
import { BacktraceFileAttachment } from './BacktraceFileAttachment.js';
@@ -15,15 +15,15 @@ export class FileAttachmentsManager implements BacktraceModule {
1515
private _attachmentsManager?: AttachmentManager;
1616

1717
constructor(
18-
private readonly _fileSystem: FileSystem,
18+
private readonly _storage: BacktraceStorage,
1919
private _fileName?: string,
2020
) {}
2121

22-
public static create(fileSystem: FileSystem) {
23-
return new FileAttachmentsManager(fileSystem);
22+
public static create(storage: BacktraceStorage) {
23+
return new FileAttachmentsManager(storage);
2424
}
2525

26-
public static createFromSession(sessionFiles: SessionFiles, fileSystem: FileSystem) {
26+
public static createFromSession(sessionFiles: SessionFiles, fileSystem: BacktraceStorage) {
2727
const fileName = sessionFiles.getFileName(ATTACHMENT_FILE_NAME);
2828
return new FileAttachmentsManager(fileSystem, fileName);
2929
}
@@ -56,7 +56,10 @@ export class FileAttachmentsManager implements BacktraceModule {
5656
}
5757

5858
try {
59-
const content = await this._fileSystem.readFile(this._fileName);
59+
const content = await this._storage.get(this._fileName);
60+
if (!content) {
61+
return [];
62+
}
6063
const attachments = JSON.parse(content) as SavedAttachment[];
6164
return attachments.map(([path, name]) => new BacktraceFileAttachment(path, name));
6265
} catch {
@@ -74,6 +77,6 @@ export class FileAttachmentsManager implements BacktraceModule {
7477
.filter((f): f is BacktraceFileAttachment => f instanceof BacktraceFileAttachment)
7578
.map<SavedAttachment>((f) => [f.filePath, f.name]);
7679

77-
await this._fileSystem.writeFile(this._fileName, JSON.stringify(fileAttachments));
80+
await this._storage.set(this._fileName, JSON.stringify(fileAttachments));
7881
}
7982
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { BacktraceAttachment } from '@backtrace/sdk-core';
2+
import { BacktraceFileAttachment } from './BacktraceFileAttachment.js';
3+
4+
export function isFileAttachment(attachment: BacktraceAttachment): attachment is BacktraceFileAttachment {
5+
return (
6+
attachment instanceof BacktraceFileAttachment ||
7+
('filePath' in attachment && typeof attachment.filePath === 'string')
8+
);
9+
}

packages/node/src/breadcrumbs/FileBreadcrumbsStorage.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
BacktraceAttachment,
33
BacktraceAttachmentProvider,
4+
BacktraceStorage,
5+
BacktraceSyncStorage,
46
Breadcrumb,
57
BreadcrumbLogLevel,
68
BreadcrumbsStorage,
@@ -15,7 +17,7 @@ import {
1517
import path from 'path';
1618
import { Readable, Writable } from 'stream';
1719
import { BacktraceFileAttachment } from '../attachment/index.js';
18-
import { NodeFileSystem } from '../storage/interfaces/NodeFileSystem.js';
20+
import { BacktraceStreamStorage } from '../storage/BacktraceStorage.js';
1921
import { chunkifier, ChunkSplitterFactory } from '../streams/chunkifier.js';
2022
import { combinedChunkSplitter } from '../streams/combinedChunkSplitter.js';
2123
import { FileChunkSink } from '../streams/fileChunkSink.js';
@@ -36,7 +38,7 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage {
3638

3739
constructor(
3840
session: SessionFiles,
39-
private readonly _fileSystem: NodeFileSystem,
41+
private readonly _storage: BacktraceStorage & BacktraceSyncStorage & BacktraceStreamStorage,
4042
private readonly _limits: BreadcrumbsStorageLimits,
4143
) {
4244
const splitters: ChunkSplitterFactory[] = [];
@@ -52,7 +54,7 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage {
5254

5355
this._sink = new FileChunkSink({
5456
maxFiles: 2,
55-
fs: this._fileSystem,
57+
storage: this._storage,
5658
file: (n) => session.getFileName(FileBreadcrumbsStorage.getFileName(n)),
5759
});
5860

@@ -71,22 +73,28 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage {
7173
});
7274
}
7375

74-
public static getSessionAttachments(session: SessionFiles, fileSystem?: NodeFileSystem) {
76+
public static getSessionAttachments(
77+
session: SessionFiles,
78+
storage?: BacktraceStorage & BacktraceSyncStorage & BacktraceStreamStorage,
79+
) {
7580
const files = session
7681
.getSessionFiles()
7782
.filter((f) => path.basename(f).startsWith(FILE_PREFIX))
7883
.slice(0, 2);
7984

80-
return files.map((file) => new BacktraceFileAttachment(file, path.basename(file), fileSystem));
85+
return files.map((file) => new BacktraceFileAttachment(file, path.basename(file), storage));
8186
}
8287

83-
public static factory(session: SessionFiles, fileSystem: NodeFileSystem): BreadcrumbsStorageFactory {
84-
return ({ limits }) => new FileBreadcrumbsStorage(session, fileSystem, limits);
88+
public static factory(
89+
session: SessionFiles,
90+
storage: BacktraceStorage & BacktraceSyncStorage & BacktraceStreamStorage,
91+
): BreadcrumbsStorageFactory {
92+
return ({ limits }) => new FileBreadcrumbsStorage(session, storage, limits);
8593
}
8694

8795
public getAttachments(): BacktraceAttachment<Readable>[] {
8896
const files = [...this._sink.files].map((f) => f.path.toString('utf-8'));
89-
return files.map((f) => new BacktraceFileAttachment(f, path.basename(f), this._fileSystem));
97+
return files.map((f) => new BacktraceFileAttachment(f, path.basename(f), this._storage));
9098
}
9199

92100
public getAttachmentProviders(): BacktraceAttachmentProvider[] {

0 commit comments

Comments
 (0)