Skip to content

Commit

Permalink
fix(sdk): Transfer large envelopes (tens of MB) over bridge without c…
Browse files Browse the repository at this point in the history
…rash (#2852)
  • Loading branch information
krystofwoldrich committed Nov 30, 2023
1 parent 886c728 commit f15994e
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 51 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Fixes

- Encode envelopes using Base64, fix array length limit when transferring over Bridge. ([#2852](https://github.com/getsentry/sentry-react-native/pull/2852))
- This fix requires a rebuild of the native app

## 5.14.1

### Fixes
Expand Down
8 changes: 3 additions & 5 deletions android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import io.sentry.protocol.User;
import io.sentry.protocol.ViewHierarchy;
import io.sentry.util.JsonSerializationUtils;
import io.sentry.vendor.Base64;

public class RNSentryModuleImpl {

Expand Down Expand Up @@ -339,11 +340,8 @@ public void fetchNativeFrames(Promise promise) {
}
}

public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise promise) {
byte[] bytes = new byte[rawBytes.size()];
for (int i = 0; i < rawBytes.size(); i++) {
bytes[i] = (byte) rawBytes.getInt(i);
}
public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) {
byte[] bytes = Base64.decode(rawBytes, Base64.DEFAULT);

try {
InternalSentrySdk.captureEnvelope(bytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void fetchNativeFrames(Promise promise) {
}

@Override
public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise promise) {
public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) {
this.impl.captureEnvelope(rawBytes, options, promise);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public void fetchNativeFrames(Promise promise) {
}

@ReactMethod
public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise promise) {
public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) {
this.impl.captureEnvelope(rawBytes, options, promise);
}

Expand Down
8 changes: 2 additions & 6 deletions ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -396,16 +396,12 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
});
}

RCT_EXPORT_METHOD(captureEnvelope:(NSArray * _Nonnull)bytes
RCT_EXPORT_METHOD(captureEnvelope:(NSString * _Nonnull)rawBytes
options: (NSDictionary * _Nonnull)options
resolve:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSMutableData *data = [[NSMutableData alloc] initWithCapacity: [bytes count]];
for(NSNumber *number in bytes) {
char byte = [number charValue];
[data appendBytes: &byte length: 1];
}
NSData *data = [[NSData alloc] initWithBase64EncodedString:rawBytes options:0];

SentryEnvelope *envelope = [PrivateSentrySDKOnly envelopeWithData:data];
if (envelope == nil) {
Expand Down
2 changes: 1 addition & 1 deletion src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes';
export interface Spec extends TurboModule {
addBreadcrumb(breadcrumb: UnsafeObject): void;
captureEnvelope(
bytes: number[],
bytes: string,
options: {
store: boolean;
},
Expand Down
73 changes: 73 additions & 0 deletions src/js/vendor/base64-js/fromByteArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable */

// The MIT License (MIT)

// Copyright (c) 2014 Jameson Little

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// Adapted from https://github.dev/beatgammit/base64-js/blob/88957c9943c7e2a0f03cdf73e71d579e433627d3/index.js#L119

const lookup: string[] = [];

const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
for (let i = 0, len = code.length; i < len; ++i) {
lookup[i] = code[i];
}

function tripletToBase64(num: number): string {
return lookup[(num >> 18) & 0x3f] + lookup[(num >> 12) & 0x3f] + lookup[(num >> 6) & 0x3f] + lookup[num & 0x3f];
}

function encodeChunk(uint8: Uint8Array | number[], start: number, end: number): string {
let tmp;
const output = [];
for (let i = start; i < end; i += 3) {
tmp = ((uint8[i] << 16) & 0xff0000) + ((uint8[i + 1] << 8) & 0xff00) + (uint8[i + 2] & 0xff);
output.push(tripletToBase64(tmp));
}
return output.join('');
}

/**
* Converts a Uint8Array or Array of bytes into a string representation of base64.
*/
export function base64StringFromByteArray(uint8: Uint8Array | number[]): string {
let tmp;
const len = uint8.length;
const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
const parts = [];
const maxChunkLength = 16383; // must be multiple of 3

// go through the array every three bytes, we'll deal with trailing stuff later
for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
parts.push(encodeChunk(uint8, i, i + maxChunkLength > len2 ? len2 : i + maxChunkLength));
}

// pad the end with zeros, but make sure to not forget the extra bytes
if (extraBytes === 1) {
tmp = uint8[len - 1];
parts.push(`${lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f]}==`);
} else if (extraBytes === 2) {
tmp = (uint8[len - 2] << 8) + uint8[len - 1];
parts.push(`${lookup[tmp >> 10] + lookup[(tmp >> 4) & 0x3f] + lookup[(tmp << 2) & 0x3f]}=`);
}

return parts.join('');
}
1 change: 1 addition & 0 deletions src/js/vendor/base64-js/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { base64StringFromByteArray } from './fromByteArray';
2 changes: 2 additions & 0 deletions src/js/vendor/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { utf8ToBytes } from './buffer';

export { base64StringFromByteArray } from './base64-js';
32 changes: 20 additions & 12 deletions src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type * as Hermes from './profiling/hermes';
import type { NativeProfileEvent } from './profiling/nativeTypes';
import type { RequiredKeysUser } from './user';
import { isTurboModuleEnabled } from './utils/environment';
import { utf8ToBytes } from './vendor';
import { base64StringFromByteArray, utf8ToBytes } from './vendor';

const RNSentry: Spec | undefined = isTurboModuleEnabled()
? TurboModuleRegistry.get<Spec>('RNSentry')
Expand Down Expand Up @@ -93,6 +93,8 @@ interface SentryNativeWrapper {
fetchNativeStackFramesBy(instructionsAddr: number[]): Promise<NativeStackFrames | null>;
}

const EOL = utf8ToBytes('\n');

/**
* Our internal interface for calling native functions
*/
Expand Down Expand Up @@ -125,27 +127,27 @@ export const NATIVE: SentryNativeWrapper = {
throw this._NativeClientError;
}

const [EOL] = utf8ToBytes('\n');

const [envelopeHeader, envelopeItems] = envelope;

const headerString = JSON.stringify(envelopeHeader);
let envelopeBytes: number[] = utf8ToBytes(headerString);
envelopeBytes.push(EOL);
const headerBytes = utf8ToBytes(headerString);
let envelopeBytes: Uint8Array = new Uint8Array(headerBytes.length + EOL.length);
envelopeBytes.set(headerBytes);
envelopeBytes.set(EOL, headerBytes.length);

let hardCrashed: boolean = false;
for (const rawItem of envelopeItems) {
const [itemHeader, itemPayload] = this._processItem(rawItem);

let bytesContentType: string;
let bytesPayload: number[] = [];
let bytesPayload: number[] | Uint8Array | undefined;
if (typeof itemPayload === 'string') {
bytesContentType = 'text/plain';
bytesPayload = utf8ToBytes(itemPayload);
} else if (itemPayload instanceof Uint8Array) {
bytesContentType =
typeof itemHeader.content_type === 'string' ? itemHeader.content_type : 'application/octet-stream';
bytesPayload = [...itemPayload];
bytesPayload = itemPayload;
} else {
bytesContentType = 'application/json';
bytesPayload = utf8ToBytes(JSON.stringify(itemPayload));
Expand All @@ -159,13 +161,19 @@ export const NATIVE: SentryNativeWrapper = {
(itemHeader as BaseEnvelopeItemHeaders).length = bytesPayload.length;
const serializedItemHeader = JSON.stringify(itemHeader);

envelopeBytes.push(...utf8ToBytes(serializedItemHeader));
envelopeBytes.push(EOL);
envelopeBytes = envelopeBytes.concat(bytesPayload);
envelopeBytes.push(EOL);
const bytesItemHeader = utf8ToBytes(serializedItemHeader);
const newBytes = new Uint8Array(
envelopeBytes.length + bytesItemHeader.length + EOL.length + bytesPayload.length + EOL.length,
);
newBytes.set(envelopeBytes);
newBytes.set(bytesItemHeader, envelopeBytes.length);
newBytes.set(EOL, envelopeBytes.length + bytesItemHeader.length);
newBytes.set(bytesPayload, envelopeBytes.length + bytesItemHeader.length + EOL.length);
newBytes.set(EOL, envelopeBytes.length + bytesItemHeader.length + EOL.length + bytesPayload.length);
envelopeBytes = newBytes;
}

await RNSentry.captureEnvelope(envelopeBytes, { store: hardCrashed });
await RNSentry.captureEnvelope(base64StringFromByteArray(envelopeBytes), { store: hardCrashed });
},

/**
Expand Down
38 changes: 38 additions & 0 deletions test/vendor/base64-js/big-data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// The MIT License (MIT)

// Copyright (c) 2014 Jameson Little

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// Adapted from https://github.dev/beatgammit/base64-js/blob/88957c9943c7e2a0f03cdf73e71d579e433627d3/test/big-data.js#L4

import { base64StringFromByteArray } from '../../../src/js/vendor';

describe('base64-js', () => {
test('convert big data to base64', () => {
const SIZE_2MB = 2e6; // scaled down from original 64MiB
const big = new Uint8Array(SIZE_2MB);
for (let i = 0, length = big.length; i < length; ++i) {
big[i] = i % 256;
}
const b64str = base64StringFromByteArray(big);
const arr = Uint8Array.from(Buffer.from(b64str, 'base64'));
expect(arr).toEqual(big);
});
});
78 changes: 78 additions & 0 deletions test/vendor/base64-js/convert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// The MIT License (MIT)

// Copyright (c) 2014 Jameson Little

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// Adapted from https://github.dev/beatgammit/base64-js/blob/88957c9943c7e2a0f03cdf73e71d579e433627d3/test/convert.js#L15

import { base64StringFromByteArray } from '../../../src/js/vendor';

describe('base64-js', () => {
const checks = ['a', 'aa', 'aaa', 'hi', 'hi!', 'hi!!', 'sup', 'sup?', 'sup?!'];

test('convert to base64 and back', () => {
for (const check of checks) {
const b64Str = base64StringFromByteArray(
map(check, function (char: string) {
return char.charCodeAt(0);
}),
);

const str = Buffer.from(b64Str, 'base64').toString();

expect(check).toEqual(str);
}
});

const data: [number[], string][] = [
[[0, 0, 0], 'AAAA'],
[[0, 0, 1], 'AAAB'],
[[0, 1, -1], 'AAH/'],
[[1, 1, 1], 'AQEB'],
[[0, -73, 23], 'ALcX'],
];

test('convert known data to string', () => {
for (const check of data) {
const bytes = check[0];
const expected = check[1];
const actual = base64StringFromByteArray(bytes);
expect(actual).toEqual(expected);
}
});

function map(arr: string, callback: (char: string) => number): number[] {
const res = [];
let kValue, mappedValue;

for (let k = 0, len = arr.length; k < len; k++) {
if (typeof arr === 'string' && !!arr.charAt(k)) {
kValue = arr.charAt(k);
mappedValue = callback(kValue);
res[k] = mappedValue;
} else if (typeof arr !== 'string' && k in arr) {
kValue = arr[k];
mappedValue = callback(kValue);
res[k] = mappedValue;
}
}
return res;
}
});
Loading

0 comments on commit f15994e

Please sign in to comment.