diff --git a/packages/node/src/BacktraceClient.ts b/packages/node/src/BacktraceClient.ts index e1b223dc..86c5eb59 100644 --- a/packages/node/src/BacktraceClient.ts +++ b/packages/node/src/BacktraceClient.ts @@ -7,49 +7,105 @@ import { SessionFiles, VariableDebugIdMapProvider, } from '@backtrace/sdk-core'; -import path from 'path'; +import nodeFs from 'fs'; import { BacktraceConfiguration, BacktraceSetupConfiguration } from './BacktraceConfiguration.js'; import { BacktraceNodeRequestHandler } from './BacktraceNodeRequestHandler.js'; import { AGENT } from './agentDefinition.js'; +import { + BacktraceFileAttachmentFactory, + NodeFsBacktraceFileAttachmentFactory, +} from './attachment/BacktraceFileAttachment.js'; import { FileAttachmentsManager } from './attachment/FileAttachmentsManager.js'; import { transformAttachment } from './attachment/transformAttachments.js'; import { FileBreadcrumbsStorage } from './breadcrumbs/FileBreadcrumbsStorage.js'; import { BacktraceClientBuilder } from './builder/BacktraceClientBuilder.js'; import { BacktraceNodeClientSetup } from './builder/BacktraceClientSetup.js'; import { NodeOptionReader } from './common/NodeOptionReader.js'; +import { toArray } from './common/asyncGenerator.js'; import { NodeDiagnosticReportConverter } from './converter/NodeDiagnosticReportConverter.js'; -import { FsNodeFileSystem } from './storage/FsNodeFileSystem.js'; -import { NodeFileSystem } from './storage/interfaces/NodeFileSystem.js'; +import { + AttachmentBacktraceDatabaseRecordSender, + AttachmentBacktraceDatabaseRecordSerializer, +} from './database/AttachmentBacktraceDatabaseRecord.js'; +import { + ReportBacktraceDatabaseRecordWithAttachmentsFactory, + ReportBacktraceDatabaseRecordWithAttachmentsSender, + ReportBacktraceDatabaseRecordWithAttachmentsSerializer, +} from './database/ReportBacktraceDatabaseRecordWithAttachments.js'; +import { assertDatabasePath } from './database/utils.js'; +import { BacktraceStorageModule } from './storage/BacktraceStorage.js'; +import { BacktraceStorageModuleFactory } from './storage/BacktraceStorageModuleFactory.js'; +import { NodeFsBacktraceStorageModuleFactory } from './storage/NodeFsBacktraceStorage.js'; +import { NodeFs } from './storage/nodeFs.js'; export class BacktraceClient extends BacktraceCoreClient { private _listeners: Record = {}; - protected get nodeFileSystem() { - return this.fileSystem as NodeFileSystem | undefined; + protected readonly storageFactory: BacktraceStorageModuleFactory; + protected readonly fileAttachmentFactory: BacktraceFileAttachmentFactory; + protected readonly fs: NodeFs; + + protected get databaseNodeFsStorage() { + return this.databaseStorage as BacktraceStorageModule | undefined; } constructor(clientSetup: BacktraceNodeClientSetup) { - const fileSystem = clientSetup.fileSystem ?? new FsNodeFileSystem(); + const storageFactory = clientSetup.storageFactory ?? new NodeFsBacktraceStorageModuleFactory(); + const fs = clientSetup.fs ?? nodeFs; + const fileAttachmentFactory = new NodeFsBacktraceFileAttachmentFactory(fs); + const storage = + clientSetup.database?.storage ?? + (clientSetup.options.database?.enable + ? storageFactory.create({ + path: assertDatabasePath(clientSetup.options.database.path), + createDirectory: clientSetup.options.database.createDatabaseDirectory, + fs, + }) + : undefined); + super({ sdkOptions: AGENT, requestHandler: new BacktraceNodeRequestHandler(clientSetup.options), debugIdMapProvider: new VariableDebugIdMapProvider(global as DebugIdContainer), + database: + clientSetup.options.database?.enable && storage + ? { + storage, + reportRecordFactory: ReportBacktraceDatabaseRecordWithAttachmentsFactory.default(), + ...clientSetup.database, + recordSenders: (submission) => ({ + report: new ReportBacktraceDatabaseRecordWithAttachmentsSender(submission), + attachment: new AttachmentBacktraceDatabaseRecordSender(submission), + ...clientSetup.database?.recordSenders?.(submission), + }), + recordSerializers: { + report: new ReportBacktraceDatabaseRecordWithAttachmentsSerializer(fileAttachmentFactory), + attachment: new AttachmentBacktraceDatabaseRecordSerializer(fileAttachmentFactory), + ...clientSetup.database?.recordSerializers, + }, + } + : undefined, ...clientSetup, - fileSystem, options: { ...clientSetup.options, - attachments: clientSetup.options.attachments?.map(transformAttachment), + attachments: clientSetup.options.attachments?.map(transformAttachment(fileAttachmentFactory)), }, }); + this.storageFactory = storageFactory; + this.fileAttachmentFactory = fileAttachmentFactory; + this.fs = fs; + const breadcrumbsManager = this.modules.get(BreadcrumbsManager); - if (breadcrumbsManager && this.sessionFiles) { - breadcrumbsManager.setStorage(FileBreadcrumbsStorage.factory(this.sessionFiles, fileSystem)); + if (breadcrumbsManager && this.sessionFiles && storage) { + breadcrumbsManager.setStorage( + FileBreadcrumbsStorage.factory(this.sessionFiles, storage, this.fileAttachmentFactory), + ); } - if (this.sessionFiles && clientSetup.options.database?.captureNativeCrashes) { - this.addModule(FileAttributeManager, FileAttributeManager.create(fileSystem)); - this.addModule(FileAttachmentsManager, FileAttachmentsManager.create(fileSystem)); + if (this.sessionFiles && storage && clientSetup.options.database?.captureNativeCrashes) { + this.addModule(FileAttributeManager, FileAttributeManager.create(storage)); + this.addModule(FileAttachmentsManager, FileAttachmentsManager.create(storage, fileAttachmentFactory)); } } @@ -58,6 +114,7 @@ export class BacktraceClient extends BacktraceCoreClient try { super.initialize(); + this.captureUnhandledErrors( this.options.captureUnhandledErrors, this.options.captureUnhandledPromiseRejections, @@ -242,18 +299,19 @@ export class BacktraceClient extends BacktraceCoreClient } private async loadNodeCrashes() { - if (!this.database || !this.nodeFileSystem || !this.options.database?.captureNativeCrashes) { + if (!this.database || !this.options.database?.captureNativeCrashes) { return; } const reportName = process.report?.filename; - const databasePath = process.report?.directory - ? process.report.directory - : (this.options.database?.path ?? process.cwd()); + const storage = this.storageFactory.create({ + path: process.report?.directory ? process.report.directory : (this.options.database?.path ?? process.cwd()), + fs: this.fs, + }); let databaseFiles: string[]; try { - databaseFiles = await this.nodeFileSystem.readDir(databasePath); + databaseFiles = await toArray(storage.keys()); } catch { return; } @@ -271,11 +329,14 @@ export class BacktraceClient extends BacktraceCoreClient const reports: [path: string, report: BacktraceReport, sessionFiles?: SessionFiles][] = []; for (const recordName of recordNames) { - const recordPath = path.join(databasePath, recordName); try { - const recordJson = await this.nodeFileSystem.readFile(recordPath); + const recordJson = await storage.get(recordName); + if (!recordJson) { + continue; + } + const report = converter.convert(JSON.parse(recordJson)); - reports.push([recordPath, report]); + reports.push([recordName, report]); } catch { // Do nothing, skip the report } @@ -292,17 +353,21 @@ export class BacktraceClient extends BacktraceCoreClient currentSession = currentSession?.getPreviousSession(); } - for (const [recordPath, report, session] of reports) { + for (const [recordName, report, session] of reports) { try { if (session) { report.attachments.push( - ...FileBreadcrumbsStorage.getSessionAttachments(session, this.nodeFileSystem), + ...FileBreadcrumbsStorage.getSessionAttachments(session, this.fileAttachmentFactory), ); - const fileAttributes = FileAttributeManager.createFromSession(session, this.nodeFileSystem); + const fileAttributes = FileAttributeManager.createFromSession(session, storage); Object.assign(report.attributes, await fileAttributes.get()); - const fileAttachments = FileAttachmentsManager.createFromSession(session, this.nodeFileSystem); + const fileAttachments = FileAttachmentsManager.createFromSession( + session, + storage, + this.fileAttachmentFactory, + ); report.attachments.push(...(await fileAttachments.get())); report.attributes['application.session'] = session.sessionId; @@ -318,7 +383,7 @@ export class BacktraceClient extends BacktraceCoreClient // Do nothing, skip the report } finally { try { - await this.nodeFileSystem.unlink(recordPath); + await storage.remove(recordName); } catch { // Do nothing } diff --git a/packages/node/src/BacktraceConfiguration.ts b/packages/node/src/BacktraceConfiguration.ts index b7f39251..d3bb1980 100644 --- a/packages/node/src/BacktraceConfiguration.ts +++ b/packages/node/src/BacktraceConfiguration.ts @@ -1,10 +1,37 @@ -import { BacktraceAttachment, BacktraceConfiguration as CoreConfiguration } from '@backtrace/sdk-core'; +import { + BacktraceAttachment, + BacktraceConfiguration as CoreConfiguration, + DisabledBacktraceDatabaseConfiguration as CoreDisabledBacktraceDatabaseConfiguration, + EnabledBacktraceDatabaseConfiguration as CoreEnabledBacktraceDatabaseConfiguration, +} from '@backtrace/sdk-core'; import { Readable } from 'stream'; -export interface BacktraceSetupConfiguration extends Omit { +export interface EnabledBacktraceDatabaseConfiguration extends CoreEnabledBacktraceDatabaseConfiguration { + /** + * Path where the SDK can store data. + */ + path: string; + /** + * Determine if the directory should be auto created by the SDK. + * @default true + */ + createDatabaseDirectory?: boolean; +} + +export interface DisabledBacktraceDatabaseConfiguration + extends CoreDisabledBacktraceDatabaseConfiguration, + Omit, 'enable'> {} + +export type BacktraceDatabaseConfiguration = + | EnabledBacktraceDatabaseConfiguration + | DisabledBacktraceDatabaseConfiguration; + +export interface BacktraceSetupConfiguration extends Omit { attachments?: Array | string>; + database?: BacktraceDatabaseConfiguration; } -export interface BacktraceConfiguration extends Omit { +export interface BacktraceConfiguration extends Omit { attachments?: BacktraceAttachment[]; + database?: BacktraceDatabaseConfiguration; } diff --git a/packages/node/src/attachment/BacktraceFileAttachment.ts b/packages/node/src/attachment/BacktraceFileAttachment.ts index c507eff6..424bce33 100644 --- a/packages/node/src/attachment/BacktraceFileAttachment.ts +++ b/packages/node/src/attachment/BacktraceFileAttachment.ts @@ -1,24 +1,37 @@ -import { BacktraceFileAttachment as CoreBacktraceFileAttachment } from '@backtrace/sdk-core'; -import fs from 'fs'; +import { BacktraceAttachment } from '@backtrace/sdk-core'; +import nodeFs from 'fs'; import path from 'path'; import { Readable } from 'stream'; -import { NodeFileSystem } from '../storage/interfaces/NodeFileSystem.js'; +import { NodeFs } from '../storage/nodeFs.js'; -export class BacktraceFileAttachment implements CoreBacktraceFileAttachment { +export class BacktraceFileAttachment implements BacktraceAttachment { public readonly name: string; constructor( public readonly filePath: string, name?: string, - private readonly _fileSystem?: NodeFileSystem, + private readonly _fs: Pick = nodeFs, ) { this.name = name ?? path.basename(this.filePath); } public get(): Readable | undefined { - if (!(this._fileSystem ?? fs).existsSync(this.filePath)) { + if (!this._fs.existsSync(this.filePath)) { return undefined; } - return (this._fileSystem ?? fs).createReadStream(this.filePath); + + return this._fs.createReadStream(this.filePath); + } +} + +export interface BacktraceFileAttachmentFactory { + create(filePath: string, name?: string): BacktraceFileAttachment; +} + +export class NodeFsBacktraceFileAttachmentFactory implements BacktraceFileAttachmentFactory { + constructor(private readonly _fs: Pick = nodeFs) {} + + public create(filePath: string, name?: string): BacktraceFileAttachment { + return new BacktraceFileAttachment(filePath, name, this._fs); } } diff --git a/packages/node/src/attachment/FileAttachmentsManager.ts b/packages/node/src/attachment/FileAttachmentsManager.ts index cde89149..83a24f69 100644 --- a/packages/node/src/attachment/FileAttachmentsManager.ts +++ b/packages/node/src/attachment/FileAttachmentsManager.ts @@ -2,10 +2,10 @@ import { AttachmentManager, BacktraceModule, BacktraceModuleBindData, - FileSystem, + BacktraceStorage, SessionFiles, } from '@backtrace/sdk-core'; -import { BacktraceFileAttachment } from './BacktraceFileAttachment.js'; +import { BacktraceFileAttachment, BacktraceFileAttachmentFactory } from './BacktraceFileAttachment.js'; const ATTACHMENT_FILE_NAME = 'bt-attachments'; @@ -15,17 +15,22 @@ export class FileAttachmentsManager implements BacktraceModule { private _attachmentsManager?: AttachmentManager; constructor( - private readonly _fileSystem: FileSystem, + private readonly _storage: BacktraceStorage, + private readonly _fileAttachmentFactory: BacktraceFileAttachmentFactory, private _fileName?: string, ) {} - public static create(fileSystem: FileSystem) { - return new FileAttachmentsManager(fileSystem); + public static create(storage: BacktraceStorage, fileAttachmentFactory: BacktraceFileAttachmentFactory) { + return new FileAttachmentsManager(storage, fileAttachmentFactory); } - public static createFromSession(sessionFiles: SessionFiles, fileSystem: FileSystem) { + public static createFromSession( + sessionFiles: SessionFiles, + fileSystem: BacktraceStorage, + fileAttachmentFactory: BacktraceFileAttachmentFactory, + ) { const fileName = sessionFiles.getFileName(ATTACHMENT_FILE_NAME); - return new FileAttachmentsManager(fileSystem, fileName); + return new FileAttachmentsManager(fileSystem, fileAttachmentFactory, fileName); } public initialize(): void { @@ -56,9 +61,12 @@ export class FileAttachmentsManager implements BacktraceModule { } try { - const content = await this._fileSystem.readFile(this._fileName); + const content = await this._storage.get(this._fileName); + if (!content) { + return []; + } const attachments = JSON.parse(content) as SavedAttachment[]; - return attachments.map(([path, name]) => new BacktraceFileAttachment(path, name)); + return attachments.map(([path, name]) => this._fileAttachmentFactory.create(path, name)); } catch { return []; } @@ -74,6 +82,6 @@ export class FileAttachmentsManager implements BacktraceModule { .filter((f): f is BacktraceFileAttachment => f instanceof BacktraceFileAttachment) .map((f) => [f.filePath, f.name]); - await this._fileSystem.writeFile(this._fileName, JSON.stringify(fileAttachments)); + await this._storage.set(this._fileName, JSON.stringify(fileAttachments)); } } diff --git a/packages/node/src/attachment/isFileAttachment.ts b/packages/node/src/attachment/isFileAttachment.ts new file mode 100644 index 00000000..dc462c9b --- /dev/null +++ b/packages/node/src/attachment/isFileAttachment.ts @@ -0,0 +1,9 @@ +import { BacktraceAttachment } from '@backtrace/sdk-core'; +import { BacktraceFileAttachment } from './BacktraceFileAttachment.js'; + +export function isFileAttachment(attachment: BacktraceAttachment): attachment is BacktraceFileAttachment { + return ( + attachment instanceof BacktraceFileAttachment || + ('filePath' in attachment && typeof attachment.filePath === 'string') + ); +} diff --git a/packages/node/src/attachment/transformAttachments.ts b/packages/node/src/attachment/transformAttachments.ts index fe3f1907..f98a189b 100644 --- a/packages/node/src/attachment/transformAttachments.ts +++ b/packages/node/src/attachment/transformAttachments.ts @@ -1,15 +1,17 @@ import { BacktraceAttachment } from '@backtrace/sdk-core'; import { Readable } from 'stream'; import { BacktraceSetupConfiguration } from '../BacktraceConfiguration.js'; -import { BacktraceFileAttachment } from './BacktraceFileAttachment.js'; +import { BacktraceFileAttachmentFactory } from './BacktraceFileAttachment.js'; /** * Transform a client attachment into the attachment model. */ -export function transformAttachment( - attachment: NonNullable[number] | BacktraceAttachment, -): BacktraceAttachment { - return typeof attachment === 'string' - ? new BacktraceFileAttachment(attachment) - : (attachment as BacktraceAttachment); -} +export const transformAttachment = + (fileAttachmentFactory: BacktraceFileAttachmentFactory) => + ( + attachment: NonNullable[number] | BacktraceAttachment, + ): BacktraceAttachment => { + return typeof attachment === 'string' + ? fileAttachmentFactory.create(attachment) + : (attachment as BacktraceAttachment); + }; diff --git a/packages/node/src/breadcrumbs/FileBreadcrumbsStorage.ts b/packages/node/src/breadcrumbs/FileBreadcrumbsStorage.ts index d0d694a7..5e7d9c33 100644 --- a/packages/node/src/breadcrumbs/FileBreadcrumbsStorage.ts +++ b/packages/node/src/breadcrumbs/FileBreadcrumbsStorage.ts @@ -1,6 +1,8 @@ import { BacktraceAttachment, BacktraceAttachmentProvider, + BacktraceStorage, + BacktraceSyncStorage, Breadcrumb, BreadcrumbLogLevel, BreadcrumbsStorage, @@ -14,8 +16,8 @@ import { } from '@backtrace/sdk-core'; import path from 'path'; import { Readable, Writable } from 'stream'; -import { BacktraceFileAttachment } from '../attachment/index.js'; -import { NodeFileSystem } from '../storage/interfaces/NodeFileSystem.js'; +import { BacktraceFileAttachmentFactory } from '../attachment/index.js'; +import { BacktraceStreamStorage } from '../storage/BacktraceStorage.js'; import { chunkifier, ChunkSplitterFactory } from '../streams/chunkifier.js'; import { combinedChunkSplitter } from '../streams/combinedChunkSplitter.js'; import { FileChunkSink } from '../streams/fileChunkSink.js'; @@ -36,7 +38,8 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { constructor( session: SessionFiles, - private readonly _fileSystem: NodeFileSystem, + private readonly _storage: BacktraceStorage & BacktraceSyncStorage & BacktraceStreamStorage, + private readonly _fileAttachmentFactory: BacktraceFileAttachmentFactory, private readonly _limits: BreadcrumbsStorageLimits, ) { const splitters: ChunkSplitterFactory[] = []; @@ -52,7 +55,7 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { this._sink = new FileChunkSink({ maxFiles: 2, - fs: this._fileSystem, + storage: this._storage, file: (n) => session.getFileName(FileBreadcrumbsStorage.getFileName(n)), }); @@ -71,22 +74,26 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { }); } - public static getSessionAttachments(session: SessionFiles, fileSystem?: NodeFileSystem) { + public static getSessionAttachments(session: SessionFiles, fileAttachmentFactory: BacktraceFileAttachmentFactory) { const files = session .getSessionFiles() .filter((f) => path.basename(f).startsWith(FILE_PREFIX)) .slice(0, 2); - return files.map((file) => new BacktraceFileAttachment(file, path.basename(file), fileSystem)); + return files.map((file) => fileAttachmentFactory.create(file, path.basename(file))); } - public static factory(session: SessionFiles, fileSystem: NodeFileSystem): BreadcrumbsStorageFactory { - return ({ limits }) => new FileBreadcrumbsStorage(session, fileSystem, limits); + public static factory( + session: SessionFiles, + storage: BacktraceStorage & BacktraceSyncStorage & BacktraceStreamStorage, + fileAttachmentFactory: BacktraceFileAttachmentFactory, + ): BreadcrumbsStorageFactory { + return ({ limits }) => new FileBreadcrumbsStorage(session, storage, fileAttachmentFactory, limits); } public getAttachments(): BacktraceAttachment[] { const files = [...this._sink.files].map((f) => f.path.toString('utf-8')); - return files.map((f) => new BacktraceFileAttachment(f, path.basename(f), this._fileSystem)); + return files.map((f) => this._fileAttachmentFactory.create(f, path.basename(f))); } public getAttachmentProviders(): BacktraceAttachmentProvider[] { diff --git a/packages/node/src/builder/BacktraceClientBuilder.ts b/packages/node/src/builder/BacktraceClientBuilder.ts index 1bf45008..30621509 100644 --- a/packages/node/src/builder/BacktraceClientBuilder.ts +++ b/packages/node/src/builder/BacktraceClientBuilder.ts @@ -1,5 +1,4 @@ import { BacktraceCoreClientBuilder } from '@backtrace/sdk-core'; -import { transformAttachment } from '../attachment/transformAttachments.js'; import { ApplicationInformationAttributeProvider, LinuxProcessStatusAttributeProvider, @@ -9,15 +8,22 @@ import { ProcessStatusAttributeProvider, } from '../attributes/index.js'; import { BacktraceClient } from '../BacktraceClient.js'; +import { BacktraceSetupConfiguration } from '../BacktraceConfiguration.js'; +import { BacktraceStorageModuleFactory } from '../storage/BacktraceStorageModuleFactory.js'; import { BacktraceClientSetup, BacktraceNodeClientSetup } from './BacktraceClientSetup.js'; export class BacktraceClientBuilder extends BacktraceCoreClientBuilder { + private attachments: BacktraceSetupConfiguration['attachments']; + private storageFactory?: BacktraceStorageModuleFactory; + constructor(clientSetup: BacktraceNodeClientSetup) { super({ ...clientSetup, - options: { ...clientSetup.options, attachments: clientSetup.options.attachments?.map(transformAttachment) }, + options: { ...clientSetup.options, attachments: [] }, }); + this.attachments = clientSetup.options.attachments; + this.addAttributeProvider(new ApplicationInformationAttributeProvider()); this.addAttributeProvider(new ProcessStatusAttributeProvider()); this.addAttributeProvider(new MachineAttributeProvider()); @@ -26,8 +32,20 @@ export class BacktraceClientBuilder extends BacktraceCoreClientBuilder {} -export type BacktraceNodeClientSetup = Omit & { +export type BacktraceNodeClientSetup = Omit & { readonly options: BacktraceSetupConfiguration; - readonly fileSystem?: NodeFileSystem; + readonly storageFactory?: BacktraceStorageModuleFactory; + readonly fs?: NodeFs; + readonly database?: Omit, 'storage'> & { + readonly storage?: BacktraceStorageModule; + }; }; diff --git a/packages/node/src/common/asyncGenerator.ts b/packages/node/src/common/asyncGenerator.ts new file mode 100644 index 00000000..4aee5f33 --- /dev/null +++ b/packages/node/src/common/asyncGenerator.ts @@ -0,0 +1,7 @@ +export async function toArray(generator: AsyncGenerator): Promise { + const result: T[] = []; + for await (const element of generator) { + result.push(element); + } + return result; +} diff --git a/packages/node/src/database/AttachmentBacktraceDatabaseRecord.ts b/packages/node/src/database/AttachmentBacktraceDatabaseRecord.ts new file mode 100644 index 00000000..d6136c59 --- /dev/null +++ b/packages/node/src/database/AttachmentBacktraceDatabaseRecord.ts @@ -0,0 +1,100 @@ +import { + BacktraceAttachment, + BacktraceDatabaseRecord, + BacktraceDatabaseRecordFactory, + BacktraceReportSubmission, + BacktraceReportSubmissionResult, + BacktraceSubmitResponse, + jsonEscaper, + SessionId, +} from '@backtrace/sdk-core'; +import { BacktraceDatabaseRecordSender } from '@backtrace/sdk-core/lib/modules/database/BacktraceDatabaseRecordSender.js'; +import { BacktraceDatabaseRecordSerializer } from '@backtrace/sdk-core/lib/modules/database/BacktraceDatabaseRecordSerializer.js'; +import { BacktraceFileAttachmentFactory } from '../attachment/BacktraceFileAttachment.js'; +import { isFileAttachment } from '../attachment/isFileAttachment.js'; + +export interface AttachmentBacktraceDatabaseRecord extends BacktraceDatabaseRecord<'attachment'> { + readonly rxid: string; + readonly attachment: BacktraceAttachment; + readonly sessionId: SessionId; +} + +export class AttachmentBacktraceDatabaseRecordSerializer + implements BacktraceDatabaseRecordSerializer +{ + public readonly type = 'attachment'; + + constructor(private readonly _fileAttachmentFactory: BacktraceFileAttachmentFactory) {} + + public save(record: AttachmentBacktraceDatabaseRecord): string | undefined { + if (!isFileAttachment(record.attachment)) { + return undefined; + } + + return JSON.stringify(record, jsonEscaper()); + } + + public load(json: string): AttachmentBacktraceDatabaseRecord | undefined { + try { + const record = JSON.parse(json) as BacktraceDatabaseRecord; + if (record.type !== this.type) { + return undefined; + } + + const attachmentRecord = record as AttachmentBacktraceDatabaseRecord; + if (!isFileAttachment(attachmentRecord.attachment)) { + return undefined; + } + + const attachment = this._fileAttachmentFactory.create( + attachmentRecord.attachment.filePath, + attachmentRecord.attachment.name, + ); + + return { + ...attachmentRecord, + attachment, + }; + } catch { + return undefined; + } + } +} + +export class AttachmentBacktraceDatabaseRecordSender + implements BacktraceDatabaseRecordSender +{ + public readonly type = 'attachment'; + + constructor(private readonly _reportSubmission: BacktraceReportSubmission) {} + + public send( + record: AttachmentBacktraceDatabaseRecord, + abortSignal?: AbortSignal, + ): Promise> { + return this._reportSubmission.sendAttachment(record.rxid, record.attachment, abortSignal); + } +} + +export class AttachmentBacktraceDatabaseRecordFactory { + constructor(private readonly _reportFactory: BacktraceDatabaseRecordFactory) {} + + public static default() { + return new AttachmentBacktraceDatabaseRecordFactory(new BacktraceDatabaseRecordFactory()); + } + + public create( + rxid: string, + sessionId: SessionId, + attachment: BacktraceAttachment, + ): AttachmentBacktraceDatabaseRecord { + const record: AttachmentBacktraceDatabaseRecord = { + ...this._reportFactory.create('attachment'), + sessionId, + rxid, + attachment, + }; + + return record; + } +} diff --git a/packages/node/src/database/ReportBacktraceDatabaseRecordWithAttachments.ts b/packages/node/src/database/ReportBacktraceDatabaseRecordWithAttachments.ts new file mode 100644 index 00000000..bedf7e88 --- /dev/null +++ b/packages/node/src/database/ReportBacktraceDatabaseRecordWithAttachments.ts @@ -0,0 +1,101 @@ +import { + BacktraceAttachment, + BacktraceData, + BacktraceDatabaseRecord, + BacktraceReportSubmission, + BacktraceReportSubmissionResult, + BacktraceSubmitResponse, + DefaultReportBacktraceDatabaseRecordFactory, + jsonEscaper, + ReportBacktraceDatabaseRecord, + ReportBacktraceDatabaseRecordFactory, +} from '@backtrace/sdk-core'; +import { BacktraceDatabaseRecordSender } from '@backtrace/sdk-core/lib/modules/database/BacktraceDatabaseRecordSender.js'; +import { BacktraceDatabaseRecordSerializer } from '@backtrace/sdk-core/lib/modules/database/BacktraceDatabaseRecordSerializer.js'; +import { BacktraceFileAttachment, BacktraceFileAttachmentFactory } from '../attachment/BacktraceFileAttachment.js'; +import { isFileAttachment } from '../attachment/isFileAttachment.js'; + +export interface ReportBacktraceDatabaseRecordWithAttachments extends ReportBacktraceDatabaseRecord { + readonly attachments: BacktraceAttachment[]; +} + +export class ReportBacktraceDatabaseRecordWithAttachmentsSerializer + implements BacktraceDatabaseRecordSerializer +{ + public readonly type = 'report'; + + constructor(private readonly _fileAttachmentFactory: BacktraceFileAttachmentFactory) {} + + public save(record: ReportBacktraceDatabaseRecordWithAttachments): string { + return JSON.stringify( + { + ...record, + attachments: record.attachments.filter(isFileAttachment), + } satisfies ReportBacktraceDatabaseRecordWithAttachments, + jsonEscaper(), + ); + } + + public load(json: string): ReportBacktraceDatabaseRecordWithAttachments | undefined { + try { + const record = JSON.parse(json) as BacktraceDatabaseRecord; + if (record.type !== this.type) { + return undefined; + } + + const reportRecord = record as ReportBacktraceDatabaseRecordWithAttachments; + if (reportRecord.attachments) { + return { + ...reportRecord, + attachments: reportRecord.attachments + .filter(isFileAttachment) + .map((a) => this._fileAttachmentFactory.create(a.filePath, a.name)), + }; + } + + return { + ...reportRecord, + attachments: [], + }; + } catch { + return undefined; + } + } +} + +export class ReportBacktraceDatabaseRecordWithAttachmentsSender + implements BacktraceDatabaseRecordSender +{ + public readonly type = 'report'; + + constructor(private readonly _reportSubmission: BacktraceReportSubmission) {} + + public send( + record: ReportBacktraceDatabaseRecordWithAttachments, + abortSignal?: AbortSignal, + ): Promise> { + return this._reportSubmission.send(record.data, record.attachments, abortSignal); + } +} + +export class ReportBacktraceDatabaseRecordWithAttachmentsFactory implements ReportBacktraceDatabaseRecordFactory { + constructor(private readonly _defaultFactory: ReportBacktraceDatabaseRecordFactory) {} + + public static default() { + return new ReportBacktraceDatabaseRecordWithAttachmentsFactory( + DefaultReportBacktraceDatabaseRecordFactory.default(), + ); + } + + public create( + data: BacktraceData, + attachments: BacktraceAttachment[], + ): ReportBacktraceDatabaseRecordWithAttachments { + const record = this._defaultFactory.create(data, attachments); + + return { + ...record, + attachments: attachments.filter((a) => a instanceof BacktraceFileAttachment), + }; + } +} diff --git a/packages/node/src/database/index.ts b/packages/node/src/database/index.ts new file mode 100644 index 00000000..0c4ade51 --- /dev/null +++ b/packages/node/src/database/index.ts @@ -0,0 +1,2 @@ +export * from './AttachmentBacktraceDatabaseRecord.js'; +export * from './ReportBacktraceDatabaseRecordWithAttachments.js'; diff --git a/packages/node/src/database/utils.ts b/packages/node/src/database/utils.ts new file mode 100644 index 00000000..8ac2d567 --- /dev/null +++ b/packages/node/src/database/utils.ts @@ -0,0 +1,8 @@ +export function assertDatabasePath(path: string) { + if (!path) { + throw new Error( + 'Missing mandatory path to the database. Please define the database.path option in the configuration.', + ); + } + return path; +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 8837d1a5..877003c0 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -6,9 +6,9 @@ export { BacktraceStackFrame, BacktraceStackTraceConverter, BreadcrumbLogLevel, - BreadcrumbType, BreadcrumbsEventSubscriber, BreadcrumbsManager, + BreadcrumbType, RawBreadcrumb, } from '@backtrace/sdk-core'; export * from './attachment/index.js'; @@ -19,4 +19,5 @@ export * from './BacktraceNodeRequestHandler.js'; export * from './breadcrumbs/index.js'; export * from './builder/BacktraceClientBuilder.js'; export * from './builder/BacktraceClientSetup.js'; +export * from './database/index.js'; export * from './storage/index.js'; diff --git a/packages/node/src/storage/BacktraceStorage.ts b/packages/node/src/storage/BacktraceStorage.ts new file mode 100644 index 00000000..88934965 --- /dev/null +++ b/packages/node/src/storage/BacktraceStorage.ts @@ -0,0 +1,20 @@ +import { BacktraceStorageModule as CoreBacktraceStorageModule } from '@backtrace/sdk-core'; +import nodeFs from 'fs'; +import { BacktraceConfiguration } from '../BacktraceConfiguration.js'; +import { NodeFs } from './nodeFs.js'; + +export interface ReadonlyBacktraceStreamStorage { + createReadStream(key: string): nodeFs.ReadStream; +} + +export interface BacktraceStreamStorage extends ReadonlyBacktraceStreamStorage { + createWriteStream(key: string): nodeFs.WriteStream; +} + +export type BacktraceStorageModule = CoreBacktraceStorageModule & BacktraceStreamStorage; + +export interface BacktraceStorageModuleOptions { + readonly path: string; + readonly createDirectory?: boolean; + readonly fs?: NodeFs; +} diff --git a/packages/node/src/storage/BacktraceStorageModuleFactory.ts b/packages/node/src/storage/BacktraceStorageModuleFactory.ts new file mode 100644 index 00000000..c4fefcc4 --- /dev/null +++ b/packages/node/src/storage/BacktraceStorageModuleFactory.ts @@ -0,0 +1,5 @@ +import { BacktraceStorageModule, BacktraceStorageModuleOptions } from './BacktraceStorage.js'; + +export interface BacktraceStorageModuleFactory { + create(options: BacktraceStorageModuleOptions): BacktraceStorageModule; +} diff --git a/packages/node/src/storage/FsNodeFileSystem.ts b/packages/node/src/storage/FsNodeFileSystem.ts deleted file mode 100644 index 56772f00..00000000 --- a/packages/node/src/storage/FsNodeFileSystem.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { BacktraceAttachment } from '@backtrace/sdk-core'; -import fs from 'fs'; -import { BacktraceFileAttachment } from '../attachment/index.js'; -import { NodeFileSystem } from './interfaces/NodeFileSystem.js'; - -export class FsNodeFileSystem implements NodeFileSystem { - public readDir(dir: string): Promise { - return fs.promises.readdir(dir); - } - - public readDirSync(dir: string): string[] { - return fs.readdirSync(dir); - } - - public createDir(dir: string): Promise { - return fs.promises.mkdir(dir, { recursive: true }) as Promise; - } - - public createDirSync(dir: string): void { - fs.mkdirSync(dir, { recursive: true }); - } - - public readFile(path: string): Promise { - return fs.promises.readFile(path, 'utf-8'); - } - - public readFileSync(path: string): string { - return fs.readFileSync(path, 'utf-8'); - } - - public writeFile(path: string, content: string): Promise { - return fs.promises.writeFile(path, content); - } - - public writeFileSync(path: string, content: string): void { - fs.writeFileSync(path, content); - } - - public unlink(path: string): Promise { - return fs.promises.unlink(path); - } - - public unlinkSync(path: string): void { - fs.unlinkSync(path); - } - - public rename(oldPath: string, newPath: string): Promise { - return fs.promises.rename(oldPath, newPath); - } - - public renameSync(oldPath: string, newPath: string): void { - fs.renameSync(oldPath, newPath); - } - - public createWriteStream(path: string): fs.WriteStream { - return fs.createWriteStream(path, 'utf-8'); - } - - public createReadStream(path: string): fs.ReadStream { - return fs.createReadStream(path, 'utf-8'); - } - - public async exists(path: string): Promise { - try { - await fs.promises.stat(path); - return true; - } catch { - return false; - } - } - - public existsSync(path: string): boolean { - return fs.existsSync(path); - } - - public createAttachment(path: string, name?: string): BacktraceAttachment { - return new BacktraceFileAttachment(path, name); - } -} diff --git a/packages/node/src/storage/NodeFsBacktraceStorage.ts b/packages/node/src/storage/NodeFsBacktraceStorage.ts new file mode 100644 index 00000000..ee93cce3 --- /dev/null +++ b/packages/node/src/storage/NodeFsBacktraceStorage.ts @@ -0,0 +1,135 @@ +import nodeFs from 'fs'; +import path from 'path'; +import { BacktraceStorageModule, BacktraceStorageModuleOptions } from './BacktraceStorage.js'; +import { BacktraceStorageModuleFactory } from './BacktraceStorageModuleFactory.js'; +import { NodeFs } from './nodeFs.js'; + +export class NodeFsBacktraceStorage implements BacktraceStorageModule { + private readonly _path: string; + private readonly _fs: NodeFs; + private readonly _createDirectory: boolean; + + constructor(options: BacktraceStorageModuleOptions) { + this._path = options.path; + this._fs = options.fs ?? nodeFs; + this._createDirectory = !!options.createDirectory; + } + + public initialize() { + if (this._createDirectory) { + this._fs.mkdirSync(this._path, { recursive: true }); + } + } + + public setSync(key: string, value: string): boolean { + try { + this._fs.writeFileSync(this.resolvePath(key), value); + return true; + } catch { + return false; + } + } + + public removeSync(key: string): boolean { + try { + this._fs.unlinkSync(this.resolvePath(key)); + return true; + } catch { + return false; + } + } + + public getSync(key: string): string | undefined { + try { + return this._fs.readFileSync(this.resolvePath(key), 'utf-8'); + } catch { + return undefined; + } + } + + public hasSync(key: string): boolean { + try { + this._fs.statSync(this.resolvePath(key)); + return true; + } catch { + return false; + } + } + + public async set(key: string, value: string): Promise { + try { + await this._fs.promises.writeFile(this.resolvePath(key), value); + return true; + } catch { + return false; + } + } + + public async remove(key: string): Promise { + try { + await this._fs.promises.unlink(this.resolvePath(key)); + return true; + } catch { + return false; + } + } + + public async get(key: string): Promise { + try { + return await this._fs.promises.readFile(this.resolvePath(key), 'utf-8'); + } catch { + return undefined; + } + } + + public async has(key: string): Promise { + try { + await this._fs.promises.stat(this.resolvePath(key)); + return true; + } catch { + return false; + } + } + + public *keysSync(): Generator { + try { + for (const entry of this._fs.readdirSync(this._path, { withFileTypes: true })) { + if (entry.isFile()) { + yield entry.name; + } + } + } catch { + return; + } + } + + public async *keys(): AsyncGenerator { + try { + for (const entry of await this._fs.promises.readdir(this._path, { withFileTypes: true })) { + if (entry.isFile()) { + yield entry.name; + } + } + } catch { + return; + } + } + + public createWriteStream(key: string): nodeFs.WriteStream { + return this._fs.createWriteStream(this.resolvePath(key), 'utf-8'); + } + + public createReadStream(key: string): nodeFs.ReadStream { + return this._fs.createReadStream(this.resolvePath(key), 'utf-8'); + } + + protected resolvePath(key: string) { + return path.resolve(this._path, key); + } +} + +export class NodeFsBacktraceStorageModuleFactory implements BacktraceStorageModuleFactory { + public create(options: BacktraceStorageModuleOptions): BacktraceStorageModule { + return new NodeFsBacktraceStorage(options); + } +} diff --git a/packages/node/src/storage/index.ts b/packages/node/src/storage/index.ts index 2219c93b..e44b69ad 100644 --- a/packages/node/src/storage/index.ts +++ b/packages/node/src/storage/index.ts @@ -1,2 +1,2 @@ -export * from './FsNodeFileSystem.js'; -export * from './interfaces/NodeFileSystem.js'; +export * from './BacktraceStorage.js'; +export * from './NodeFsBacktraceStorage.js'; diff --git a/packages/node/src/storage/interfaces/NodeFileSystem.ts b/packages/node/src/storage/interfaces/NodeFileSystem.ts deleted file mode 100644 index 75b1db66..00000000 --- a/packages/node/src/storage/interfaces/NodeFileSystem.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FileSystem } from '@backtrace/sdk-core'; -import { ReadStream, WriteStream } from 'fs'; - -export interface NodeFileSystem extends FileSystem { - createReadStream(path: string): ReadStream; - createWriteStream(path: string): WriteStream; - rename(oldPath: string, newPath: string): Promise; - renameSync(oldPath: string, newPath: string): void; -} diff --git a/packages/node/src/storage/nodeFs.ts b/packages/node/src/storage/nodeFs.ts new file mode 100644 index 00000000..5d83a975 --- /dev/null +++ b/packages/node/src/storage/nodeFs.ts @@ -0,0 +1,19 @@ +import nodeFs from 'fs'; + +/** + * All used methods of the Node file system. + */ +export type NodeFs = Pick< + typeof nodeFs, + | 'mkdirSync' + | 'existsSync' + | 'writeFileSync' + | 'unlinkSync' + | 'readFileSync' + | 'statSync' + | 'readdirSync' + | 'createReadStream' + | 'createWriteStream' +> & { + readonly promises: Pick<(typeof nodeFs)['promises'], 'readdir' | 'stat' | 'writeFile' | 'unlink' | 'readFile'>; +}; diff --git a/packages/node/src/streams/fileChunkSink.ts b/packages/node/src/streams/fileChunkSink.ts index 494acabb..92ef5038 100644 --- a/packages/node/src/streams/fileChunkSink.ts +++ b/packages/node/src/streams/fileChunkSink.ts @@ -1,6 +1,7 @@ +import { BacktraceStorage } from '@backtrace/sdk-core'; import EventEmitter from 'events'; import fs from 'fs'; -import { NodeFileSystem } from '../storage/interfaces/NodeFileSystem.js'; +import { BacktraceStreamStorage } from '../storage/BacktraceStorage.js'; import { ChunkSink } from './chunkifier.js'; interface FileChunkSinkOptions { @@ -17,7 +18,7 @@ interface FileChunkSinkOptions { /** * File system implementation to use. */ - readonly fs: NodeFileSystem; + readonly storage: BacktraceStorage & BacktraceStreamStorage; } /** @@ -64,13 +65,13 @@ export class FileChunkSink extends EventEmitter { private createStream(n: number) { const path = this._options.file(n); - return (this._options.fs ?? fs).createWriteStream(path); + return this._options.storage.createWriteStream(path); } private emitDeleteOrDelete(file: fs.WriteStream) { // If 'delete' event is not handled, delete the file if (!this.emit('delete', file)) { - this._options.fs.unlink(file.path.toString('utf-8')).catch(() => { + this._options.storage.remove(file.path.toString('utf-8')).catch(() => { // Do nothing on error }); } diff --git a/packages/node/tests/_mocks/fileSystem.ts b/packages/node/tests/_mocks/storage.ts similarity index 57% rename from packages/node/tests/_mocks/fileSystem.ts rename to packages/node/tests/_mocks/storage.ts index 51e3a9a3..d92944b1 100644 --- a/packages/node/tests/_mocks/fileSystem.ts +++ b/packages/node/tests/_mocks/storage.ts @@ -1,28 +1,25 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore The following import fails due to missing extension, but it cannot have one (it imports a .ts file) -import { MockedFileSystem, mockFileSystem } from '@backtrace/sdk-core/tests/_mocks/fileSystem'; +import { mockBacktraceStorage } from '@backtrace/sdk-core/tests/_mocks/storage'; + +import { BacktraceStorageModule } from '@backtrace/sdk-core'; +import type { MockedBacktraceStorage } from '@backtrace/sdk-core/tests/_mocks/storage.js'; import { ReadStream, WriteStream } from 'fs'; -import path from 'path'; import { Readable, Writable } from 'stream'; -import { NodeFileSystem } from '../../src/storage/interfaces/NodeFileSystem.js'; +import { NodeFsBacktraceStorage } from '../../src/storage/NodeFsBacktraceStorage.js'; +import { NodeFs } from '../../src/storage/nodeFs.js'; + +type MockedFs = Pick; -export function mockStreamFileSystem(files?: Record): MockedFileSystem { - const fs = mockFileSystem(files); +export function mockNodeStorageAndFs( + files?: Record, +): MockedBacktraceStorage> { + const storage = mockBacktraceStorage(files) as MockedBacktraceStorage>; return { - ...fs, + ...storage, - rename: jest.fn().mockImplementation((oldPath: string, newPath: string) => { - const old = fs.files[path.resolve(oldPath)]; - delete fs.files[path.resolve(oldPath)]; - fs.files[path.resolve(newPath)] = old; - return Promise.resolve(); - }), - renameSync: jest.fn().mockImplementation((oldPath: string, newPath: string) => { - const old = fs.files[path.resolve(oldPath)]; - delete fs.files[path.resolve(oldPath)]; - fs.files[path.resolve(newPath)] = old; - }), + existsSync: jest.fn().mockImplementation((p: string) => p in storage.files), createWriteStream: jest.fn().mockImplementation((p: string) => { const writable = new Writable({ @@ -33,11 +30,10 @@ export function mockStreamFileSystem(files?: Record): MockedFile ? chunk : String(chunk).toString(); - const fullPath = path.resolve(p); - if (!fs.files[fullPath]) { - fs.files[fullPath] = str; + if (!storage.files[p]) { + storage.files[p] = str; } else { - fs.files[fullPath] += str; + storage.files[p] += str; } callback && callback(); @@ -48,8 +44,7 @@ export function mockStreamFileSystem(files?: Record): MockedFile }), createReadStream: jest.fn().mockImplementation((p: string) => { - const fullPath = path.resolve(p); - const file = fs.files[fullPath]; + const file = storage.files[p]; if (!file) { throw new Error(`File ${p} does not exist`); } diff --git a/packages/node/tests/breadcrumbs/FileBreadcrumbsStorage.spec.ts b/packages/node/tests/breadcrumbs/FileBreadcrumbsStorage.spec.ts index 2fc66bb2..4b521cde 100644 --- a/packages/node/tests/breadcrumbs/FileBreadcrumbsStorage.spec.ts +++ b/packages/node/tests/breadcrumbs/FileBreadcrumbsStorage.spec.ts @@ -3,7 +3,8 @@ import assert from 'assert'; import { Readable } from 'stream'; import { promisify } from 'util'; import { FileBreadcrumbsStorage } from '../../src/breadcrumbs/FileBreadcrumbsStorage.js'; -import { mockStreamFileSystem } from '../_mocks/fileSystem.js'; +import { NodeFsBacktraceFileAttachmentFactory } from '../../src/index.js'; +import { mockNodeStorageAndFs } from '../_mocks/storage.js'; async function readToEnd(readable: Readable) { return new Promise((resolve, reject) => { @@ -33,8 +34,8 @@ const nextTick = promisify(process.nextTick); describe('FileBreadcrumbsStorage', () => { it('should return added breadcrumbs', async () => { - const fs = mockStreamFileSystem(); - const session = new SessionFiles(fs, '.', 'sessionId'); + const fs = mockNodeStorageAndFs(); + const session = new SessionFiles(fs, { id: 'sessionId', timestamp: Date.now() }); const breadcrumbs: RawBreadcrumb[] = [ { @@ -86,7 +87,7 @@ describe('FileBreadcrumbsStorage', () => { }, ]; - const storage = new FileBreadcrumbsStorage(session, fs, { + const storage = new FileBreadcrumbsStorage(session, fs, new NodeFsBacktraceFileAttachmentFactory(fs), { maximumBreadcrumbs: 100, }); @@ -107,8 +108,8 @@ describe('FileBreadcrumbsStorage', () => { }); it('should return added breadcrumbs in two attachments', async () => { - const fs = mockStreamFileSystem(); - const session = new SessionFiles(fs, '.', 'sessionId'); + const fs = mockNodeStorageAndFs(); + const session = new SessionFiles(fs, { id: 'sessionId', timestamp: Date.now() }); const breadcrumbs: RawBreadcrumb[] = [ { @@ -163,7 +164,7 @@ describe('FileBreadcrumbsStorage', () => { }, ]; - const storage = new FileBreadcrumbsStorage(session, fs, { + const storage = new FileBreadcrumbsStorage(session, fs, new NodeFsBacktraceFileAttachmentFactory(fs), { maximumBreadcrumbs: 4, }); @@ -189,8 +190,8 @@ describe('FileBreadcrumbsStorage', () => { }); it('should return no more than maximumBreadcrumbs breadcrumbs', async () => { - const fs = mockStreamFileSystem(); - const session = new SessionFiles(fs, '.', 'sessionId'); + const fs = mockNodeStorageAndFs(); + const session = new SessionFiles(fs, { id: 'sessionId', timestamp: Date.now() }); const breadcrumbs: RawBreadcrumb[] = [ { @@ -235,7 +236,7 @@ describe('FileBreadcrumbsStorage', () => { }, ]; - const storage = new FileBreadcrumbsStorage(session, fs, { + const storage = new FileBreadcrumbsStorage(session, fs, new NodeFsBacktraceFileAttachmentFactory(fs), { maximumBreadcrumbs: 2, }); @@ -261,8 +262,8 @@ describe('FileBreadcrumbsStorage', () => { }); it('should return breadcrumbs up to the json size', async () => { - const fs = mockStreamFileSystem(); - const session = new SessionFiles(fs, '.', 'sessionId'); + const fs = mockNodeStorageAndFs(); + const session = new SessionFiles(fs, { id: 'sessionId', timestamp: Date.now() }); const breadcrumbs: RawBreadcrumb[] = [ { @@ -302,7 +303,7 @@ describe('FileBreadcrumbsStorage', () => { }, ]; - const storage = new FileBreadcrumbsStorage(session, fs, { + const storage = new FileBreadcrumbsStorage(session, fs, new NodeFsBacktraceFileAttachmentFactory(fs), { maximumBreadcrumbs: 100, maximumTotalBreadcrumbsSize: JSON.stringify(expectedMain[0]).length + 10, }); @@ -329,8 +330,8 @@ describe('FileBreadcrumbsStorage', () => { }); it('should return attachments with a valid name from getAttachments', async () => { - const fs = mockStreamFileSystem(); - const session = new SessionFiles(fs, '.', 'sessionId'); + const fs = mockNodeStorageAndFs(); + const session = new SessionFiles(fs, { id: 'sessionId', timestamp: Date.now() }); const breadcrumbs: RawBreadcrumb[] = [ { @@ -354,7 +355,7 @@ describe('FileBreadcrumbsStorage', () => { }, ]; - const storage = new FileBreadcrumbsStorage(session, fs, { + const storage = new FileBreadcrumbsStorage(session, fs, new NodeFsBacktraceFileAttachmentFactory(fs), { maximumBreadcrumbs: 4, }); @@ -373,8 +374,8 @@ describe('FileBreadcrumbsStorage', () => { }); it('should return attachments with a valid name from getAttachmentProviders', async () => { - const fs = mockStreamFileSystem(); - const session = new SessionFiles(fs, '.', 'sessionId'); + const fs = mockNodeStorageAndFs(); + const session = new SessionFiles(fs, { id: 'sessionId', timestamp: Date.now() }); const breadcrumbs: RawBreadcrumb[] = [ { @@ -398,7 +399,7 @@ describe('FileBreadcrumbsStorage', () => { }, ]; - const storage = new FileBreadcrumbsStorage(session, fs, { + const storage = new FileBreadcrumbsStorage(session, fs, new NodeFsBacktraceFileAttachmentFactory(fs), { maximumBreadcrumbs: 4, }); diff --git a/packages/node/tests/streams/fileChunkSink.spec.ts b/packages/node/tests/streams/fileChunkSink.spec.ts index d35b94dd..a3e457ff 100644 --- a/packages/node/tests/streams/fileChunkSink.spec.ts +++ b/packages/node/tests/streams/fileChunkSink.spec.ts @@ -1,7 +1,6 @@ -import path from 'path'; import { Writable } from 'stream'; import { FileChunkSink } from '../../src/streams/fileChunkSink.js'; -import { mockStreamFileSystem } from '../_mocks/fileSystem.js'; +import { mockNodeStorageAndFs } from '../_mocks/storage.js'; function writeAndClose(stream: Writable, value: string) { return new Promise((resolve, reject) => { @@ -18,18 +17,17 @@ function sortString(a: string, b: string) { describe('fileChunkSink', () => { it('should create a filestream with name from filename', async () => { - const fs = mockStreamFileSystem(); + const fs = mockNodeStorageAndFs(); const filename = 'abc'; - const sink = new FileChunkSink({ file: () => filename, maxFiles: Infinity, fs }); + const sink = new FileChunkSink({ file: () => filename, maxFiles: Infinity, storage: fs }); const stream = sink.getSink()(0); expect(stream.path).toEqual(filename); }); it('should create a filestream each time it is called', async () => { - const fs = mockStreamFileSystem(); - const dir = 'test'; - const sink = new FileChunkSink({ file: (n) => path.join(dir, n.toString()), maxFiles: Infinity, fs }); + const fs = mockNodeStorageAndFs(); + const sink = new FileChunkSink({ file: (n) => n.toString(), maxFiles: Infinity, storage: fs }); const expected = [0, 2, 5]; for (const n of expected) { @@ -37,15 +35,14 @@ describe('fileChunkSink', () => { await writeAndClose(stream, 'a'); } - const actual = await fs.readDir(dir); + const actual = [...fs.keysSync()]; expect(actual.sort(sortString)).toEqual(expected.map((e) => e.toString()).sort(sortString)); }); it('should remove previous files if count exceeds maxFiles', async () => { - const fs = mockStreamFileSystem(); - const dir = 'test'; + const fs = mockNodeStorageAndFs(); const maxFiles = 3; - const sink = new FileChunkSink({ file: (n) => path.join(dir, n.toString()), maxFiles, fs }); + const sink = new FileChunkSink({ file: (n) => n.toString(), maxFiles, storage: fs }); const files = [0, 2, 5, 6, 79, 81, 38, -1, 3]; const expected = files.slice(-maxFiles); @@ -54,7 +51,7 @@ describe('fileChunkSink', () => { await writeAndClose(stream, 'a'); } - const actual = await fs.readDir(dir); + const actual = [...fs.keysSync()]; expect(actual.sort(sortString)).toEqual(expected.map((e) => e.toString()).sort(sortString)); }); });