Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) Add Attachments #2463

Merged
merged 23 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ef71954
(feat) Add Attachments
krystofwoldrich Sep 8, 2022
b7afc14
Add attachment header to ios
krystofwoldrich Sep 9, 2022
42c67eb
Add basic attachment support to ios, fix lint
krystofwoldrich Sep 9, 2022
ec39b54
Remove propagation of attachments to native sdks
krystofwoldrich Sep 12, 2022
c2b06e9
Add attachment processing for android
krystofwoldrich Sep 12, 2022
eb2ad99
Add new captureEnvelope bridge draft
krystofwoldrich Sep 12, 2022
15ee4b7
Add capture envelope native implementation
krystofwoldrich Sep 13, 2022
0ff0f1d
Remove getStringBytesLength from bridge
krystofwoldrich Sep 13, 2022
43b3d10
Fix tests
krystofwoldrich Sep 13, 2022
d98a589
Fix lint, add vendor buffer desc
krystofwoldrich Sep 13, 2022
5c317cc
Add utf8 tests
krystofwoldrich Sep 13, 2022
57de079
Fix changelog
krystofwoldrich Sep 13, 2022
20ce45c
Remove Buffer
krystofwoldrich Sep 13, 2022
681927a
Add first draft asset attachment example
krystofwoldrich Sep 13, 2022
27e7b92
Add ios attachments example, fix js sample dep on Buffer
krystofwoldrich Sep 14, 2022
ed285cc
Fix lint
krystofwoldrich Sep 14, 2022
cb91e25
Add simple byte size utf8 check
krystofwoldrich Sep 14, 2022
89893cf
Update CHANGELOG.md
krystofwoldrich Sep 14, 2022
d55170d
Refactor sample HomeScreen attachment example
krystofwoldrich Sep 14, 2022
9147cb5
Add buffer license
krystofwoldrich Sep 15, 2022
a0a7e1b
Remove options store ignored log
krystofwoldrich Sep 15, 2022
5906a26
Add missing envelope message test and fix breadcrumbs
krystofwoldrich Sep 15, 2022
c9fbe21
Merge remote-tracking branch 'origin/main' into krystofwoldrich/attac…
krystofwoldrich Sep 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions android/src/main/java/io/sentry/react/RNSentryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableMap;
Expand Down Expand Up @@ -292,7 +293,16 @@ public void fetchNativeFrames(Promise promise) {
}

@ReactMethod
public void captureEnvelope(String envelope, Promise promise) {
public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise promise) {
if (!options.getBoolean("store")) {
logger.warning("Envelopes are always stored.");
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

byte bytes[] = new byte[rawBytes.size()];
for (int i = 0; i < rawBytes.size(); i++) {
bytes[i] = (byte) rawBytes.getInt(i);
}

try {
final String outboxPath = HubAdapter.getInstance().getOptions().getOutboxPath();

Expand All @@ -302,24 +312,15 @@ public void captureEnvelope(String envelope, Promise promise) {
} else {
File installation = new File(outboxPath, UUID.randomUUID().toString());
try (FileOutputStream out = new FileOutputStream(installation)) {
out.write(envelope.getBytes(Charset.forName("UTF-8")));
out.write(bytes);
}
}
} catch (Throwable ignored) {
logger.severe("Error reading envelope");
logger.severe("Error while writing envelope to outbox.");
}
promise.resolve(true);
}

@ReactMethod
public void getStringBytesLength(String payload, Promise promise) {
try {
promise.resolve(payload.getBytes("UTF-8").length);
} catch (UnsupportedEncodingException e) {
promise.reject(e);
}
}

krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
private static PackageInfo getPackageInfo(Context ctx) {
try {
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
Expand Down
57 changes: 22 additions & 35 deletions ios/RNSentry.m
Original file line number Diff line number Diff line change
Expand Up @@ -270,47 +270,34 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
});
}

RCT_EXPORT_METHOD(captureEnvelope:(NSDictionary * _Nonnull)envelopeDict
RCT_EXPORT_METHOD(captureEnvelope:(NSArray * _Nonnull)bytes
options: (NSDictionary * _Nonnull)options
resolve:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
if ([NSJSONSerialization isValidJSONObject:envelopeDict]) {
SentrySdkInfo *sdkInfo = [[SentrySdkInfo alloc] initWithDict:envelopeDict[@"header"]];
SentryId *eventId = [[SentryId alloc] initWithUUIDString:envelopeDict[@"header"][@"event_id"]];
SentryTraceContext *traceContext = [[SentryTraceContext alloc] initWithDict:envelopeDict[@"header"][@"trace"]];
SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:eventId sdkInfo:sdkInfo traceContext:traceContext];

NSError *error;
NSData *envelopeItemData = [NSJSONSerialization dataWithJSONObject:envelopeDict[@"payload"] options:0 error:&error];
if (nil != error) {
reject(@"SentryReactNative", @"Cannot serialize event", error);
}

NSString *itemType = envelopeDict[@"payload"][@"type"];
if (itemType == nil) {
// Default to event type.
itemType = @"event";
}

SentryEnvelopeItemHeader *envelopeItemHeader = [[SentryEnvelopeItemHeader alloc] initWithType:itemType length:envelopeItemData.length];
SentryEnvelopeItem *envelopeItem = [[SentryEnvelopeItem alloc] initWithHeader:envelopeItemHeader data:envelopeItemData];
NSMutableData *data = [[NSMutableData alloc] initWithCapacity: [bytes count]];
for(NSNumber *number in bytes) {
char byte = [number charValue];
[data appendBytes: &byte length: 1];
}

SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader singleItem:envelopeItem];
SentryEnvelope *envelope = [PrivateSentrySDKOnly envelopeWithData:data];
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
if (envelope == nil) {
reject(@"SentryReactNative",@"Failed to parse envelope from byte array.", nil);
return;
}

#if DEBUG
#if DEBUG
[PrivateSentrySDKOnly captureEnvelope:envelope];
#else
if (options[@'store']) {
// Storing to disk happens asynchronously with captureEnvelope
[PrivateSentrySDKOnly storeEnvelope:envelope];
} else {
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
[PrivateSentrySDKOnly captureEnvelope:envelope];
#else
if ([envelopeDict[@"payload"][@"level"] isEqualToString:@"fatal"]) {
// Storing to disk happens asynchronously with captureEnvelope
[PrivateSentrySDKOnly storeEnvelope:envelope];
} else {
[PrivateSentrySDKOnly captureEnvelope:envelope];
}
#endif
resolve(@YES);
} else {
reject(@"SentryReactNative", @"Cannot serialize event", nil);
}
}
#endif
resolve(@YES);
}

RCT_EXPORT_METHOD(setUser:(NSDictionary *)user
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"@sentry/tracing": "7.12.1",
"@sentry/types": "7.12.1",
"@sentry/utils": "7.12.1",
"@sentry/wizard": "1.2.17"
"@sentry/wizard": "1.2.17",
"buffer": "6.0.3"
},
"devDependencies": {
"@sentry-internal/eslint-config-sdk": "7.12.1",
Expand Down
4 changes: 2 additions & 2 deletions sample/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Sentry.init({
// Replace the example DSN below with your own DSN:
dsn: SENTRY_INTERNAL_DSN,
debug: true,
beforeSend: (e) => {
console.log('Event beforeSend:', e);
beforeSend: (e, hint) => {
console.log('Event beforeSend:', e, 'hint:', hint);
return e;
},
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
Expand Down
20 changes: 20 additions & 0 deletions sample/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as Sentry from '@sentry/react-native';
import { getTestProps } from '../../utils/getTestProps';
import { SENTRY_INTERNAL_DSN } from '../dsn';
import { SeverityLevel } from '@sentry/types';
import { Scope } from '@sentry/react-native';

interface Props {
navigation: StackNavigationProp<any, 'HomeScreen'>;
Expand Down Expand Up @@ -223,6 +224,25 @@ const HomeScreen = (props: Props) => {
</Text>
</TouchableOpacity>
</Sentry.ErrorBoundary>
<View style={styles.spacer} />
<TouchableOpacity
onPress={async () => {
Sentry.configureScope((scope: Scope) => {
scope.addAttachment({ data: 'Attachment content', filename: 'attachment.txt' });
console.log('Sentry attachment added.');
});
}}>
<Text style={styles.buttonText}>Add attachment</Text>
</TouchableOpacity>
<View style={styles.spacer} />
<TouchableOpacity
onPress={async () => {
Sentry.configureScope((scope: Scope) => {
console.log(scope.getAttachments());
});
}}>
<Text style={styles.buttonText}>Get attachment</Text>
</TouchableOpacity>
</View>
<View style={styles.buttonArea}>
<TouchableOpacity
Expand Down
1 change: 1 addition & 0 deletions src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
this._browserClient = new BrowserClient({
dsn: options.dsn,
transport: options.transport,
transportOptions: options.transportOptions,
stackParser: options.stackParser || defaultStackParser,
integrations: [],
});
Expand Down
11 changes: 4 additions & 7 deletions src/js/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ export interface SentryNativeBridgeModule {

addBreadcrumb(breadcrumb: Breadcrumb): void;
captureEnvelope(
payload:
| string
| {
header: Record<string, unknown>;
payload: Record<string, unknown>;
}
bytes: number[],
options: {
store: boolean,
},
): PromiseLike<boolean>;
clearBreadcrumbs(): void;
crash(): void;
Expand All @@ -53,7 +51,6 @@ export interface SentryNativeBridgeModule {
fetchNativeDeviceContexts(): PromiseLike<NativeDeviceContextsResponse>;
fetchNativeAppStart(): PromiseLike<NativeAppStartResponse | null>;
fetchNativeFrames(): PromiseLike<NativeFramesResponse | null>;
getStringBytesLength(str: string): Promise<number>;
initNativeSdk(options: ReactNativeOptions): Promise<boolean>;
setUser(
defaultUserKeys: SerializedObject | null,
Expand Down
4 changes: 3 additions & 1 deletion src/js/integrations/reactnativeerrorhandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export class ReactNativeErrorHandlers implements Integration {

const currentHub = getCurrentHub();
const client = currentHub.getClient<ReactNativeClient>();
const scope = currentHub.getScope();

if (!client) {
logger.error(
Expand All @@ -201,7 +202,8 @@ export class ReactNativeErrorHandlers implements Integration {
const options = client.getOptions();

const event = await client.eventFromException(error, {
originalException: error
originalException: error,
attachments: scope?.getAttachments(),
});

if (isFatal) {
Expand Down
16 changes: 15 additions & 1 deletion src/js/scope.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Scope } from '@sentry/hub';
import { Breadcrumb, User } from '@sentry/types';
import { Attachment, Breadcrumb, User } from '@sentry/types';

import { NATIVE } from './wrapper';

Expand Down Expand Up @@ -78,4 +78,18 @@ export class ReactNativeScope extends Scope {
NATIVE.setContext(key, context);
return super.setContext(key, context);
}

/**
* @inheritDoc
*/
public addAttachment(attachment: Attachment): this {
return super.addAttachment(attachment);
}

/**
* @inheritDoc
*/
public clearAttachments(): this {
return super.clearAttachments();
}
}
10 changes: 9 additions & 1 deletion src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ReactNativeScope } from './scope';
import { TouchEventBoundary } from './touchevents';
import { ReactNativeProfiler, ReactNativeTracing } from './tracing';
import { makeReactNativeTransport } from './transports/native';
import { makeUtf8TextEncoder } from './transports/TextEncoder';

const IGNORED_DEFAULT_INTEGRATIONS = [
'GlobalHandlers', // We will use the react-native internal handlers
Expand All @@ -32,7 +33,10 @@ const DEFAULT_OPTIONS: ReactNativeOptions = {
autoInitializeNativeSdk: true,
enableAutoPerformanceTracking: true,
enableOutOfMemoryTracking: true,
patchGlobalPromise: true
patchGlobalPromise: true,
transportOptions: {
textEncoder: makeUtf8TextEncoder(),
},
};

/**
Expand All @@ -46,6 +50,10 @@ export function init(passedOptions: ReactNativeOptions): void {
...DEFAULT_OPTIONS,
...passedOptions,
transport: passedOptions.transport || makeReactNativeTransport,
transportOptions: {
...DEFAULT_OPTIONS.transportOptions,
...(passedOptions.transportOptions ?? {}),
},
integrations: getIntegrationsToSetup(passedOptions),
stackParser: stackParserFromStackParserOptions(passedOptions.stackParser || defaultStackParser)
};
Expand Down
13 changes: 13 additions & 0 deletions src/js/transports/TextEncoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TextEncoderInternal } from '@sentry/types';
import { Buffer } from 'buffer';

export const makeUtf8TextEncoder = (): TextEncoderInternal => {
const textEncoder = {
encode: (text: string) => {
const bytes = new Uint8Array(Buffer.from(text, 'utf8'));
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
return bytes;
},
encoding: 'utf-8',
};
return textEncoder;
}
3 changes: 3 additions & 0 deletions src/js/vendor/buffer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {
utf8ToBytes,
} from './utf8ToBytes';
89 changes: 89 additions & 0 deletions src/js/vendor/buffer/utf8ToBytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-disable */

/**
* Convert a string to a byte array
*
* This is a utf8ToBytes function from the buffer module (with added types)
* https://github.com/feross/buffer/blob/795bbb5bda1b39f1370ebd784bea6107b087e3a7/index.js#L1956
*
* License: MIT (https://github.com/feross/buffer)
*/
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
export function utf8ToBytes(string: string, units?: number): number[] {
units = units || Infinity
let codePoint
const length = string.length
let leadSurrogate = null
const bytes: number[] = []

for (let i = 0; i < length; ++i) {
codePoint = string.charCodeAt(i)

// is surrogate component
if (codePoint > 0xD7FF && codePoint < 0xE000) {
// last char was a lead
if (!leadSurrogate) {
// no lead yet
if (codePoint > 0xDBFF) {
// unexpected trail
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
continue
} else if (i + 1 === length) {
// unpaired lead
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
continue
}

// valid lead
leadSurrogate = codePoint

continue
}

// 2 leads in a row
if (codePoint < 0xDC00) {
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
leadSurrogate = codePoint
continue
}

// valid surrogate pair
codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000
} else if (leadSurrogate) {
// valid bmp char, but last char was a lead
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
}

leadSurrogate = null

// encode utf8
if (codePoint < 0x80) {
if ((units -= 1) < 0) break
bytes.push(codePoint)
} else if (codePoint < 0x800) {
if ((units -= 2) < 0) break
bytes.push(
codePoint >> 0x6 | 0xC0,
codePoint & 0x3F | 0x80
)
} else if (codePoint < 0x10000) {
if ((units -= 3) < 0) break
bytes.push(
codePoint >> 0xC | 0xE0,
codePoint >> 0x6 & 0x3F | 0x80,
codePoint & 0x3F | 0x80
)
} else if (codePoint < 0x110000) {
if ((units -= 4) < 0) break
bytes.push(
codePoint >> 0x12 | 0xF0,
codePoint >> 0xC & 0x3F | 0x80,
codePoint >> 0x6 & 0x3F | 0x80,
codePoint & 0x3F | 0x80
)
} else {
throw new Error('Invalid code point')
}
}

return bytes
}
4 changes: 4 additions & 0 deletions src/js/vendor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export {
utf8ToBytes,
} from './buffer';
Loading