From 71843f86216d691b9dbee0a92107a76787bf9764 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 18 Nov 2024 09:28:21 +0100 Subject: [PATCH 1/7] react-native: add factory to FileBreadcrumbsStorage and use it --- packages/react-native/src/BacktraceClient.ts | 8 +---- .../src/breadcrumbs/FileBreadcrumbsStorage.ts | 32 +++++++++++++------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/react-native/src/BacktraceClient.ts b/packages/react-native/src/BacktraceClient.ts index c3ed8b8f..f5b589a3 100644 --- a/packages/react-native/src/BacktraceClient.ts +++ b/packages/react-native/src/BacktraceClient.ts @@ -55,13 +55,7 @@ export class BacktraceClient extends BacktraceCoreClient const breadcrumbsManager = this.modules.get(BreadcrumbsManager); if (breadcrumbsManager && this.sessionFiles) { - breadcrumbsManager.setStorage( - FileBreadcrumbsStorage.create( - fileSystem, - this.sessionFiles, - (clientSetup.options.breadcrumbs?.maximumBreadcrumbs ?? 100) || 100, - ), - ); + breadcrumbsManager.setStorage(FileBreadcrumbsStorage.factory(this.sessionFiles, fileSystem)); } this.attributeManager.attributeEvents.on( diff --git a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts index 95750f40..47fae1ca 100644 --- a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts +++ b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts @@ -8,12 +8,14 @@ import { type BacktraceAttachmentProvider, type Breadcrumb, type BreadcrumbsStorage, + type BreadcrumbsStorageFactory, + type BreadcrumbsStorageLimits, type RawBreadcrumb, } from '@backtrace/sdk-core'; import { WritableStream } from 'web-streams-polyfill'; import { BacktraceFileAttachment } from '..'; import { type FileSystem } from '../storage'; -import { ChunkifierSink } from '../storage/Chunkifier'; +import { ChunkifierSink, type ChunkSplitterFactory } from '../storage/Chunkifier'; import { FileChunkSink } from '../storage/FileChunkSink'; import { lineChunkSplitter } from '../storage/lineChunkSplitter'; @@ -32,7 +34,7 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { constructor( session: SessionFiles, private readonly _fileSystem: FileSystem, - maximumBreadcrumbs: number, + private readonly _limits: BreadcrumbsStorageLimits, ) { this._sink = new FileChunkSink({ maxFiles: 2, @@ -40,18 +42,28 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { file: (n) => session.getFileName(FileBreadcrumbsStorage.getFileName(n)), }); - this._destinationStream = new WritableStream( - new ChunkifierSink({ - sink: this._sink.getSink(), - splitter: () => lineChunkSplitter(Math.ceil(maximumBreadcrumbs / 2)), - }), - ); + const splitters: ChunkSplitterFactory[] = []; + const maximumBreadcrumbs = this._limits.maximumBreadcrumbs; + if (maximumBreadcrumbs !== undefined) { + splitters.push(() => lineChunkSplitter(Math.ceil(maximumBreadcrumbs / 2))); + } + + if (!splitters[0]) { + this._destinationStream = this._sink.getSink()(0); + } else { + this._destinationStream = new WritableStream( + new ChunkifierSink({ + sink: this._sink.getSink(), + splitter: splitters[0], + }), + ); + } this._destinationWriter = this._destinationStream.getWriter(); } - public static create(fileSystem: FileSystem, session: SessionFiles, maximumBreadcrumbs: number) { - return new FileBreadcrumbsStorage(session, fileSystem, maximumBreadcrumbs); + public static factory(session: SessionFiles, fileSystem: FileSystem): BreadcrumbsStorageFactory { + return ({ limits }) => new FileBreadcrumbsStorage(session, fileSystem, limits); } public getAttachments(): BacktraceAttachment[] { From 4815b71a2179f91986e81e059982dca9b956e7f6 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 18 Nov 2024 09:39:22 +0100 Subject: [PATCH 2/7] react-native: add lengthChunkSplitter --- .../src/storage/lengthChunkSplitter.ts | 57 ++++++++++++ .../tests/storage/lengthChunkSplitter.spec.ts | 86 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 packages/react-native/src/storage/lengthChunkSplitter.ts create mode 100644 packages/react-native/tests/storage/lengthChunkSplitter.spec.ts diff --git a/packages/react-native/src/storage/lengthChunkSplitter.ts b/packages/react-native/src/storage/lengthChunkSplitter.ts new file mode 100644 index 00000000..686ccf18 --- /dev/null +++ b/packages/react-native/src/storage/lengthChunkSplitter.ts @@ -0,0 +1,57 @@ +import type { ChunkSplitter } from './Chunkifier'; + +/** + * Splits data into chunks with maximum length. + * @param maxLength Maximum length of one chunk. + * @param wholeLines Can be one of: + * * `"skip"` - if last line does not fit in the chunk, it will be skipped entirely + * * `"break"` - if last line does not fit in the chunk, it will be broken into two new chunks + * * `false` - last line will be always broken into old and new chunk + */ +export function lengthChunkSplitter( + maxLength: number, + wholeLines: 'skip' | 'break' | false = false, +): ChunkSplitter { + let seen = 0; + + const emptyBuffer = ''; + + return function lengthChunkSplitter(data) { + const remainingLength = maxLength - seen; + if (data.length <= remainingLength) { + seen += data.length; + return [data]; + } + + seen = 0; + if (!wholeLines) { + return [data.substring(0, remainingLength), data.substring(remainingLength)]; + } + + // Check last newline before first chunk end + const lastLineIndex = data.substring(0, remainingLength).lastIndexOf('\n'); + + // If there is no newline, pass empty buffer as the first chunk + // and write all data into the second + if (lastLineIndex === -1) { + if (remainingLength !== maxLength) { + return [emptyBuffer, data]; + } + + if (wholeLines === 'break') { + // Break the line into two chunks + return [data.substring(0, remainingLength), data.substring(remainingLength)]; + } else { + const firstNewLine = data.indexOf('\n', remainingLength); + if (firstNewLine === -1) { + return [emptyBuffer]; + } + + return [emptyBuffer, data.substring(firstNewLine + 1)]; + } + } + + // +1 - include trailing newline in first chunk, skip in second + return [data.substring(0, lastLineIndex + 1), data.substring(lastLineIndex + 1)]; + }; +} diff --git a/packages/react-native/tests/storage/lengthChunkSplitter.spec.ts b/packages/react-native/tests/storage/lengthChunkSplitter.spec.ts new file mode 100644 index 00000000..2279ea34 --- /dev/null +++ b/packages/react-native/tests/storage/lengthChunkSplitter.spec.ts @@ -0,0 +1,86 @@ +import { lengthChunkSplitter } from '../../src/storage/lengthChunkSplitter'; +import { chunkify, splitToEnd } from '../_helpers/chunks'; +import { dataStream, generatorStream, randomString, readToEnd } from '../_helpers/generators'; + +describe('lengthChunkSplitter', () => { + it('should split chunk if it is larger than maxLength', async () => { + const maxLength = 10; + const chunk = await readToEnd(randomString(30)); + const splitter = lengthChunkSplitter(maxLength); + + const [c1, c2] = splitter(chunk); + expect(c1.length).toEqual(maxLength); + expect(c2?.length).toEqual(30 - maxLength); + }); + + it('should split chunk if total seen length is larger than maxLength', async () => { + const maxLength = 100; + const chunk = await readToEnd(randomString(30)); + const splitter = lengthChunkSplitter(maxLength); + + splitter(chunk); + splitter(chunk); + splitter(chunk); + const [c1, c2] = splitter(chunk); + + expect(c1.length).toEqual(100 - 30 * 3); + expect(c2?.length).toEqual(20); + }); + + it('should not split chunk if it is smaller than maxLength', async () => { + const maxLength = 100; + const chunk = await readToEnd(randomString(30)); + const splitter = lengthChunkSplitter(maxLength); + const [c1, c2] = splitter(chunk); + + expect(c1.length).toEqual(30); + expect(c2).toBeUndefined(); + }); + + it('should not split chunk if it is equal to maxLength', async () => { + const maxLength = 100; + const chunk = await readToEnd(randomString(maxLength)); + const splitter = lengthChunkSplitter(maxLength); + const [c1, c2] = splitter(chunk); + + expect(c1.length).toEqual(maxLength); + expect(c2).toBeUndefined(); + }); + + it('should split chunk by length', async () => { + const maxLength = 123; + const data = await readToEnd(randomString(1000)); + const splitter = lengthChunkSplitter(maxLength); + const actual = await splitToEnd(generatorStream(chunkify(data, 100)), splitter); + + for (let i = 0; i < actual.length; i++) { + const chunk = actual[i]; + expect(chunk.length).toBeLessThanOrEqual(maxLength); + expect(chunk).toEqual(data.substring(i * maxLength, (i + 1) * maxLength)); + } + }); + + describe('whole lines', () => { + it('should split chunk on length with whole lines and break longer lines', async () => { + const data = 'a\nb\ncde\nfghijklmno\npqrs\ntuv\nwxyz'; + const maxLength = 4; + const expected = ['a\nb\n', 'cde\n', 'fghi', 'jklm', 'no\n', 'pqrs', '\n', 'tuv\n', 'wxyz']; + + const splitter = lengthChunkSplitter(maxLength, 'break'); + const actual = await splitToEnd(dataStream(data), splitter); + + expect(actual).toEqual(expected); + }); + + it('should split chunk on length with whole lines and skip longer lines', async () => { + const data = 'a\nb\ncde\nfghijklmno\npqrs\ntuv\nwxyz'; + const maxLength = 4; + const expected = ['a\nb\n', 'cde\n', '', '', 'tuv\n', 'wxyz']; + + const splitter = lengthChunkSplitter(maxLength, 'skip'); + const actual = await splitToEnd(dataStream(data), splitter); + + expect(actual).toEqual(expected); + }); + }); +}); From e5fb317b8af0cbc7775544fbb38b076b1e5421d5 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 18 Nov 2024 09:39:45 +0100 Subject: [PATCH 3/7] react-native: add combinedChunkSplitter, use both splitters in FileBreadcrumbsStorage --- .../src/breadcrumbs/FileBreadcrumbsStorage.ts | 13 +++++++- .../src/storage/combinedChunkSplitter.ts | 33 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/src/storage/combinedChunkSplitter.ts diff --git a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts index 47fae1ca..be06a3ba 100644 --- a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts +++ b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts @@ -16,7 +16,9 @@ import { WritableStream } from 'web-streams-polyfill'; import { BacktraceFileAttachment } from '..'; import { type FileSystem } from '../storage'; import { ChunkifierSink, type ChunkSplitterFactory } from '../storage/Chunkifier'; +import { combinedChunkSplitter } from '../storage/combinedChunkSplitter'; import { FileChunkSink } from '../storage/FileChunkSink'; +import { lengthChunkSplitter } from '../storage/lengthChunkSplitter'; import { lineChunkSplitter } from '../storage/lineChunkSplitter'; const FILE_PREFIX = 'bt-breadcrumbs'; @@ -48,13 +50,22 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { splitters.push(() => lineChunkSplitter(Math.ceil(maximumBreadcrumbs / 2))); } + const maximumTotalBreadcrumbsSize = this._limits.maximumTotalBreadcrumbsSize; + if (maximumTotalBreadcrumbsSize !== undefined) { + splitters.push(() => lengthChunkSplitter(Math.ceil(maximumTotalBreadcrumbsSize), 'skip')); + } + if (!splitters[0]) { this._destinationStream = this._sink.getSink()(0); } else { this._destinationStream = new WritableStream( new ChunkifierSink({ sink: this._sink.getSink(), - splitter: splitters[0], + splitter: + splitters.length === 1 + ? splitters[0] + : () => + combinedChunkSplitter((strs) => strs.join(''), ...splitters.map((s) => s())), }), ); } diff --git a/packages/react-native/src/storage/combinedChunkSplitter.ts b/packages/react-native/src/storage/combinedChunkSplitter.ts new file mode 100644 index 00000000..b2891edc --- /dev/null +++ b/packages/react-native/src/storage/combinedChunkSplitter.ts @@ -0,0 +1,33 @@ +import type { Chunk, ChunkSplitter } from './Chunkifier'; + +/** + * Combines several splitters into one. + * + * Each splitter is checked, in order that they are passed. + * Splitters receive always the first chunk. + * + * If more than one splitter returns splitted chunks, the second + * chunks are concatenated and treated as one chunk. + * @param splitters + * @returns + */ +export function combinedChunkSplitter( + join: (chunks: W[]) => W, + ...splitters: ChunkSplitter[] +): ChunkSplitter { + return (chunk) => { + const rest: W[] = []; + + for (const splitter of splitters) { + const [c1, c2] = splitter(chunk); + chunk = c1; + if (c2) { + // Prepend second chunk to the rest + rest.unshift(c2); + } + } + + // If any chunks are in rest, concatenate them and pass as the second chunk + return [chunk, rest.length ? join(rest) : undefined]; + }; +} From 54a854cc82ba23de7e1e7f62127a886067031123 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 18 Nov 2024 09:44:59 +0100 Subject: [PATCH 4/7] react-native: add FileBreadcrumbsStorage tests --- .../src/attachment/BacktraceFileAttachment.ts | 4 +- .../src/breadcrumbs/FileBreadcrumbsStorage.ts | 3 +- .../storage/FileBreadcrumbsStorage.spec.ts | 415 ++++++++++++++++++ 3 files changed, 418 insertions(+), 4 deletions(-) create mode 100644 packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts diff --git a/packages/react-native/src/attachment/BacktraceFileAttachment.ts b/packages/react-native/src/attachment/BacktraceFileAttachment.ts index 87fcf3c0..ff939731 100644 --- a/packages/react-native/src/attachment/BacktraceFileAttachment.ts +++ b/packages/react-native/src/attachment/BacktraceFileAttachment.ts @@ -2,7 +2,7 @@ import { type BacktraceFileAttachment as CoreBacktraceFileAttachment } from '@ba import { Platform } from 'react-native'; import { type FileSystem } from '../storage/'; import { type FileLocation } from '../types/FileLocation'; -export class BacktraceFileAttachment implements CoreBacktraceFileAttachment { +export class BacktraceFileAttachment implements CoreBacktraceFileAttachment { public readonly name: string; public readonly mimeType: string; @@ -18,7 +18,7 @@ export class BacktraceFileAttachment implements CoreBacktraceFileAttachment { this._uploadUri = Platform.OS === 'android' ? `file://${this.filePath}` : this.filePath; } - public get(): FileLocation | string | undefined { + public get(): FileLocation | undefined { const exists = this._fileSystemProvider.existsSync(this.filePath); if (!exists) { diff --git a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts index be06a3ba..151ea33c 100644 --- a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts +++ b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts @@ -4,7 +4,6 @@ import { jsonEscaper, SessionFiles, TimeHelper, - type BacktraceAttachment, type BacktraceAttachmentProvider, type Breadcrumb, type BreadcrumbsStorage, @@ -77,7 +76,7 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { return ({ limits }) => new FileBreadcrumbsStorage(session, fileSystem, limits); } - public getAttachments(): BacktraceAttachment[] { + public getAttachments(): BacktraceFileAttachment[] { const files = [...this._sink.files].map((f) => f.path); return files.map( (f, i) => new BacktraceFileAttachment(this._fileSystem, f, `bt-breadcrumbs-${i}`, 'application/json'), diff --git a/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts new file mode 100644 index 00000000..688e49e2 --- /dev/null +++ b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts @@ -0,0 +1,415 @@ +import { Breadcrumb, BreadcrumbLogLevel, BreadcrumbType, RawBreadcrumb, SessionFiles } from '@backtrace/sdk-core'; +import { MockedFileSystem } from '@backtrace/sdk-core/tests/_mocks/fileSystem'; +import assert from 'assert'; +import { promisify } from 'util'; +import { FileBreadcrumbsStorage } from '../../src/breadcrumbs/FileBreadcrumbsStorage'; +import { FileSystem } from '../../src/storage/FileSystem'; +import { FileLocation } from '../../src/types/FileLocation'; +import { mockStreamFileSystem } from '../_mocks/fileSystem'; + +async function loadBreadcrumbs(fs: MockedFileSystem, location: FileLocation): Promise { + return (await fs.readFile(location.filepath)) + .split('\n') + .filter((n) => !!n) + .map((x) => { + try { + return JSON.parse(x); + } catch (err) { + throw new Error(`failed to parse "${x}": ${err}`); + } + }); +} + +const nextTick = promisify(process.nextTick); + +describe('FileBreadcrumbsStorage', () => { + it('should return added breadcrumbs', async () => { + const fs = mockStreamFileSystem(); + const session = new SessionFiles(fs, '.', 'sessionId'); + + const breadcrumbs: RawBreadcrumb[] = [ + { + level: BreadcrumbLogLevel.Info, + message: 'a', + type: BreadcrumbType.Manual, + attributes: { + foo: 'bar', + }, + }, + { + level: BreadcrumbLogLevel.Debug, + message: 'b', + type: BreadcrumbType.Http, + }, + { + level: BreadcrumbLogLevel.Warning, + message: 'c', + type: BreadcrumbType.Navigation, + attributes: {}, + }, + ]; + + const expectedMain: Breadcrumb[] = [ + { + id: expect.any(Number), + level: 'info', + message: 'a', + timestamp: expect.any(Number), + type: 'manual', + attributes: { + foo: 'bar', + }, + }, + { + id: expect.any(Number), + level: 'debug', + message: 'b', + timestamp: expect.any(Number), + type: 'http', + }, + { + id: expect.any(Number), + level: 'warning', + message: 'c', + timestamp: expect.any(Number), + type: 'navigation', + attributes: {}, + }, + ]; + + const storage = new FileBreadcrumbsStorage(session, fs, { + maximumBreadcrumbs: 100, + }); + + for (const breadcrumb of breadcrumbs) { + storage.add(breadcrumb); + } + + // FileBreadcrumbsStorage is asynchronous in nature + await nextTick(); + + const [mainAttachment] = storage.getAttachments(); + + const mainStream = mainAttachment.get(); + assert(mainStream); + + const actualMain = await loadBreadcrumbs(fs, mainStream); + expect(actualMain).toEqual(expectedMain); + }); + + it('should return added breadcrumbs in two attachments', async () => { + const fs = mockStreamFileSystem(); + const session = new SessionFiles(fs, '.', 'sessionId'); + + const breadcrumbs: RawBreadcrumb[] = [ + { + level: BreadcrumbLogLevel.Info, + message: 'a', + type: BreadcrumbType.Manual, + attributes: { + foo: 'bar', + }, + }, + { + level: BreadcrumbLogLevel.Debug, + message: 'b', + type: BreadcrumbType.Http, + }, + { + level: BreadcrumbLogLevel.Warning, + message: 'c', + type: BreadcrumbType.Navigation, + attributes: {}, + }, + ]; + + const expectedMain: Breadcrumb[] = [ + { + id: expect.any(Number), + level: 'warning', + message: 'c', + timestamp: expect.any(Number), + type: 'navigation', + attributes: {}, + }, + ]; + + const expectedFallback: Breadcrumb[] = [ + { + id: expect.any(Number), + level: 'info', + message: 'a', + timestamp: expect.any(Number), + type: 'manual', + attributes: { + foo: 'bar', + }, + }, + { + id: expect.any(Number), + level: 'debug', + message: 'b', + timestamp: expect.any(Number), + type: 'http', + }, + ]; + + const storage = new FileBreadcrumbsStorage(session, fs, { + maximumBreadcrumbs: 4, + }); + + for (const breadcrumb of breadcrumbs) { + storage.add(breadcrumb); + await nextTick(); + } + + // FileBreadcrumbsStorage is asynchronous in nature + await nextTick(); + + const [fallbackAttachment, mainAttachment] = storage.getAttachments(); + + const mainStream = mainAttachment.get(); + const fallbackStream = fallbackAttachment.get(); + assert(mainStream); + assert(fallbackStream); + + const actualMain = await loadBreadcrumbs(fs, mainStream); + const actualFallback = await loadBreadcrumbs(fs, fallbackStream); + expect(actualMain).toEqual(expectedMain); + expect(actualFallback).toEqual(expectedFallback); + }); + + it('should return no more than maximumBreadcrumbs breadcrumbs', async () => { + const fs = mockStreamFileSystem(); + const session = new SessionFiles(fs, '.', 'sessionId'); + + const breadcrumbs: RawBreadcrumb[] = [ + { + level: BreadcrumbLogLevel.Info, + message: 'a', + type: BreadcrumbType.Manual, + attributes: { + foo: 'bar', + }, + }, + { + level: BreadcrumbLogLevel.Debug, + message: 'b', + type: BreadcrumbType.Http, + }, + { + level: BreadcrumbLogLevel.Warning, + message: 'c', + type: BreadcrumbType.Navigation, + attributes: {}, + }, + ]; + + const expectedMain: Breadcrumb[] = [ + { + id: expect.any(Number), + level: 'warning', + message: 'c', + timestamp: expect.any(Number), + type: 'navigation', + attributes: {}, + }, + ]; + + const expectedFallback: Breadcrumb[] = [ + { + id: expect.any(Number), + level: 'debug', + message: 'b', + timestamp: expect.any(Number), + type: 'http', + }, + ]; + + const storage = new FileBreadcrumbsStorage(session, fs, { + maximumBreadcrumbs: 2, + }); + + for (const breadcrumb of breadcrumbs) { + storage.add(breadcrumb); + await nextTick(); + } + + // FileBreadcrumbsStorage is asynchronous in nature + await nextTick(); + + const [fallbackAttachment, mainAttachment] = storage.getAttachments(); + + const mainStream = mainAttachment.get(); + const fallbackStream = fallbackAttachment.get(); + assert(mainStream); + assert(fallbackStream); + + const actualMain = await loadBreadcrumbs(fs, mainStream); + const actualFallback = await loadBreadcrumbs(fs, fallbackStream); + expect(actualMain).toEqual(expectedMain); + expect(actualFallback).toEqual(expectedFallback); + }); + + it('should return breadcrumbs up to the json size', async () => { + const fs = mockStreamFileSystem(); + const session = new SessionFiles(fs, '.', 'sessionId'); + + const breadcrumbs: RawBreadcrumb[] = [ + { + level: BreadcrumbLogLevel.Debug, + message: 'a', + type: BreadcrumbType.Http, + }, + { + level: BreadcrumbLogLevel.Debug, + message: 'b', + type: BreadcrumbType.Http, + }, + { + level: BreadcrumbLogLevel.Debug, + message: 'c', + type: BreadcrumbType.Http, + }, + ]; + + const expectedMain: Breadcrumb[] = [ + { + id: expect.any(Number), + level: 'debug', + message: 'c', + timestamp: expect.any(Number), + type: 'http', + }, + ]; + + const expectedFallback: Breadcrumb[] = [ + { + id: expect.any(Number), + level: 'debug', + message: 'b', + timestamp: expect.any(Number), + type: 'http', + }, + ]; + + const storage = new FileBreadcrumbsStorage(session, fs, { + maximumBreadcrumbs: 100, + maximumTotalBreadcrumbsSize: JSON.stringify(expectedMain[0]).length + 10, + }); + + for (const breadcrumb of breadcrumbs) { + storage.add(breadcrumb); + await nextTick(); + } + + // FileBreadcrumbsStorage is asynchronous in nature + await nextTick(); + + const [fallbackAttachment, mainAttachment] = storage.getAttachments(); + + const mainStream = mainAttachment?.get(); + const fallbackStream = fallbackAttachment?.get(); + assert(mainStream); + assert(fallbackStream); + + const actualMain = await loadBreadcrumbs(fs, mainStream); + const actualFallback = await loadBreadcrumbs(fs, fallbackStream); + expect(actualMain).toEqual(expectedMain); + expect(actualFallback).toEqual(expectedFallback); + }); + + it('should return attachments with a valid name from getAttachments', async () => { + const fs = mockStreamFileSystem(); + const session = new SessionFiles(fs, '.', 'sessionId'); + + const breadcrumbs: RawBreadcrumb[] = [ + { + level: BreadcrumbLogLevel.Info, + message: 'a', + type: BreadcrumbType.Manual, + attributes: { + foo: 'bar', + }, + }, + { + level: BreadcrumbLogLevel.Debug, + message: 'b', + type: BreadcrumbType.Http, + }, + { + level: BreadcrumbLogLevel.Warning, + message: 'c', + type: BreadcrumbType.Navigation, + attributes: {}, + }, + ]; + + const storage = new FileBreadcrumbsStorage(session, fs, { + maximumBreadcrumbs: 4, + }); + + for (const breadcrumb of breadcrumbs) { + storage.add(breadcrumb); + await nextTick(); + } + + // FileBreadcrumbsStorage is asynchronous in nature + await nextTick(); + + const [fallbackAttachment, mainAttachment] = storage.getAttachments(); + + expect(fallbackAttachment.name).toEqual(expect.stringMatching(/^bt-breadcrumbs-0/)); + expect(mainAttachment.name).toEqual(expect.stringMatching(/^bt-breadcrumbs-1/)); + }); + + it('should return attachments with a valid name from getAttachmentProviders', async () => { + const fs = mockStreamFileSystem(); + const session = new SessionFiles(fs, '.', 'sessionId'); + + const breadcrumbs: RawBreadcrumb[] = [ + { + level: BreadcrumbLogLevel.Info, + message: 'a', + type: BreadcrumbType.Manual, + attributes: { + foo: 'bar', + }, + }, + { + level: BreadcrumbLogLevel.Debug, + message: 'b', + type: BreadcrumbType.Http, + }, + { + level: BreadcrumbLogLevel.Warning, + message: 'c', + type: BreadcrumbType.Navigation, + attributes: {}, + }, + ]; + + const storage = new FileBreadcrumbsStorage(session, fs, { + maximumBreadcrumbs: 4, + }); + + for (const breadcrumb of breadcrumbs) { + storage.add(breadcrumb); + await nextTick(); + } + + // FileBreadcrumbsStorage is asynchronous in nature + await nextTick(); + + const providers = storage.getAttachmentProviders(); + + const [fallbackAttachment, mainAttachment] = providers + .map((v) => v.get()) + .map((v) => (Array.isArray(v) ? v : [v])) + .filter((f) => !!f) + .reduce((acc, arr) => [...acc, ...arr], []); + + expect(fallbackAttachment?.name).toEqual(expect.stringMatching(/^bt-breadcrumbs-0/)); + expect(mainAttachment?.name).toEqual(expect.stringMatching(/^bt-breadcrumbs-1/)); + }); +}); From 63b7d64a80b5c2049866755c6c42ffbb05c2c51c Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 18 Nov 2024 11:10:45 +0100 Subject: [PATCH 5/7] react-native: fix maximumTotalBreadcrumbsSize allowing 2x size --- packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts | 2 +- .../react-native/tests/storage/FileBreadcrumbsStorage.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts index 151ea33c..82ba0e5d 100644 --- a/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts +++ b/packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts @@ -51,7 +51,7 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage { const maximumTotalBreadcrumbsSize = this._limits.maximumTotalBreadcrumbsSize; if (maximumTotalBreadcrumbsSize !== undefined) { - splitters.push(() => lengthChunkSplitter(Math.ceil(maximumTotalBreadcrumbsSize), 'skip')); + splitters.push(() => lengthChunkSplitter(Math.ceil(maximumTotalBreadcrumbsSize / 2), 'skip')); } if (!splitters[0]) { diff --git a/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts index 688e49e2..93165d1b 100644 --- a/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts +++ b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts @@ -295,7 +295,7 @@ describe('FileBreadcrumbsStorage', () => { const storage = new FileBreadcrumbsStorage(session, fs, { maximumBreadcrumbs: 100, - maximumTotalBreadcrumbsSize: JSON.stringify(expectedMain[0]).length + 10, + maximumTotalBreadcrumbsSize: (JSON.stringify(expectedMain[0]).length + 10) * 2, }); for (const breadcrumb of breadcrumbs) { From 281651e22f1af8e430df3f9e1d886b2d375cd7e5 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Mon, 18 Nov 2024 16:09:18 +0100 Subject: [PATCH 6/7] react-native: add tests for breadcrumbs with lines in them (NFC) --- .../tests/storage/FileBreadcrumbsStorage.spec.ts | 10 +++++----- .../tests/storage/lineChunkSplitter.spec.ts | 13 ++++++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts index 93165d1b..2e7314c9 100644 --- a/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts +++ b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts @@ -186,7 +186,7 @@ describe('FileBreadcrumbsStorage', () => { const breadcrumbs: RawBreadcrumb[] = [ { level: BreadcrumbLogLevel.Info, - message: 'a', + message: 'a\n1', type: BreadcrumbType.Manual, attributes: { foo: 'bar', @@ -194,12 +194,12 @@ describe('FileBreadcrumbsStorage', () => { }, { level: BreadcrumbLogLevel.Debug, - message: 'b', + message: 'b\n2', type: BreadcrumbType.Http, }, { level: BreadcrumbLogLevel.Warning, - message: 'c', + message: 'c\n3', type: BreadcrumbType.Navigation, attributes: {}, }, @@ -209,7 +209,7 @@ describe('FileBreadcrumbsStorage', () => { { id: expect.any(Number), level: 'warning', - message: 'c', + message: 'c\n3', timestamp: expect.any(Number), type: 'navigation', attributes: {}, @@ -220,7 +220,7 @@ describe('FileBreadcrumbsStorage', () => { { id: expect.any(Number), level: 'debug', - message: 'b', + message: 'b\n2', timestamp: expect.any(Number), type: 'http', }, diff --git a/packages/react-native/tests/storage/lineChunkSplitter.spec.ts b/packages/react-native/tests/storage/lineChunkSplitter.spec.ts index 7939c03a..572957dd 100644 --- a/packages/react-native/tests/storage/lineChunkSplitter.spec.ts +++ b/packages/react-native/tests/storage/lineChunkSplitter.spec.ts @@ -1,6 +1,6 @@ import { lineChunkSplitter } from '../../src/storage/lineChunkSplitter'; import { chunkify, splitToEnd } from '../_helpers/chunks'; -import { generatorStream, randomLines } from '../_helpers/generators'; +import { dataStream, generatorStream, randomLines } from '../_helpers/generators'; function countNewlines(buffer: string) { return [...buffer.matchAll(/\n/g)].length; @@ -76,4 +76,15 @@ describe('lineChunkSplitter', () => { } } }); + + it('should not split escaped newlines', async () => { + const maxLines = 3; + const chunk = 'a\\n1\nb\\n2\nc\\n3\nd\\n4'; + const expected = ['a\\n1\nb\\n2\nc\\n3\n', 'd\\n4']; + + const splitter = lineChunkSplitter(maxLines); + const actual = await splitToEnd(dataStream(chunk), splitter); + + expect(actual).toEqual(expected); + }); }); From 5ff1447309b06bade065fd66b43f7fc13e4971c8 Mon Sep 17 00:00:00 2001 From: Sebastian Alex Date: Fri, 22 Nov 2024 11:19:23 +0100 Subject: [PATCH 7/7] react-native: update test case in FileBreadcrumbsStorage.spec (NFC) --- .../react-native/tests/storage/FileBreadcrumbsStorage.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts index 2e7314c9..63a194db 100644 --- a/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts +++ b/packages/react-native/tests/storage/FileBreadcrumbsStorage.spec.ts @@ -295,7 +295,8 @@ describe('FileBreadcrumbsStorage', () => { const storage = new FileBreadcrumbsStorage(session, fs, { maximumBreadcrumbs: 100, - maximumTotalBreadcrumbsSize: (JSON.stringify(expectedMain[0]).length + 10) * 2, + maximumTotalBreadcrumbsSize: + JSON.stringify(expectedMain[0]).length + JSON.stringify(expectedFallback[0]).length + 10, }); for (const breadcrumb of breadcrumbs) {