Skip to content

Commit

Permalink
react-native: reduce breadcrumb size (#320)
Browse files Browse the repository at this point in the history
* react-native: add factory to FileBreadcrumbsStorage and use it

* react-native: add lengthChunkSplitter

* react-native: add combinedChunkSplitter, use both splitters in FileBreadcrumbsStorage

* react-native: add FileBreadcrumbsStorage tests

* react-native: fix maximumTotalBreadcrumbsSize allowing 2x size

* react-native: add tests for breadcrumbs with lines in them (NFC)

* react-native: update test case in FileBreadcrumbsStorage.spec (NFC)

---------

Co-authored-by: Sebastian Alex <[email protected]>
  • Loading branch information
perf2711 and perf2711 authored Nov 22, 2024
1 parent 8ab904a commit f9890b2
Show file tree
Hide file tree
Showing 8 changed files with 641 additions and 22 deletions.
8 changes: 1 addition & 7 deletions packages/react-native/src/BacktraceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,7 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>

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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileLocation> {
public readonly name: string;
public readonly mimeType: string;

Expand All @@ -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) {
Expand Down
46 changes: 34 additions & 12 deletions packages/react-native/src/breadcrumbs/FileBreadcrumbsStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import {
jsonEscaper,
SessionFiles,
TimeHelper,
type BacktraceAttachment,
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 { combinedChunkSplitter } from '../storage/combinedChunkSplitter';
import { FileChunkSink } from '../storage/FileChunkSink';
import { lengthChunkSplitter } from '../storage/lengthChunkSplitter';
import { lineChunkSplitter } from '../storage/lineChunkSplitter';

const FILE_PREFIX = 'bt-breadcrumbs';
Expand All @@ -32,29 +35,48 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage {
constructor(
session: SessionFiles,
private readonly _fileSystem: FileSystem,
maximumBreadcrumbs: number,
private readonly _limits: BreadcrumbsStorageLimits,
) {
this._sink = new FileChunkSink({
maxFiles: 2,
fs: this._fileSystem,
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<string>[] = [];
const maximumBreadcrumbs = this._limits.maximumBreadcrumbs;
if (maximumBreadcrumbs !== undefined) {
splitters.push(() => lineChunkSplitter(Math.ceil(maximumBreadcrumbs / 2)));
}

const maximumTotalBreadcrumbsSize = this._limits.maximumTotalBreadcrumbsSize;
if (maximumTotalBreadcrumbsSize !== undefined) {
splitters.push(() => lengthChunkSplitter(Math.ceil(maximumTotalBreadcrumbsSize / 2), 'skip'));
}

if (!splitters[0]) {
this._destinationStream = this._sink.getSink()(0);
} else {
this._destinationStream = new WritableStream(
new ChunkifierSink({
sink: this._sink.getSink(),
splitter:
splitters.length === 1
? splitters[0]
: () =>
combinedChunkSplitter<string>((strs) => strs.join(''), ...splitters.map((s) => s())),
}),
);
}

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<unknown>[] {
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'),
Expand Down
33 changes: 33 additions & 0 deletions packages/react-native/src/storage/combinedChunkSplitter.ts
Original file line number Diff line number Diff line change
@@ -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<W extends Chunk>(
join: (chunks: W[]) => W,
...splitters: ChunkSplitter<W>[]
): ChunkSplitter<W> {
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];
};
}
57 changes: 57 additions & 0 deletions packages/react-native/src/storage/lengthChunkSplitter.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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)];
};
}
Loading

0 comments on commit f9890b2

Please sign in to comment.