Skip to content

Commit 396f4f7

Browse files
feat(api) Add Attachments (#2463)
* (feat) Add Attachments * Add attachment header to ios * Add basic attachment support to ios, fix lint * Remove propagation of attachments to native sdks * Add attachment processing for android * Add new captureEnvelope bridge draft * Add capture envelope native implementation * Remove getStringBytesLength from bridge * Fix tests * Fix lint, add vendor buffer desc * Add utf8 tests * Fix changelog * Remove Buffer * Add first draft asset attachment example * Add ios attachments example, fix js sample dep on Buffer * Fix lint * Add simple byte size utf8 check * Update CHANGELOG.md Co-authored-by: Manoel Aranda Neto <[email protected]> * Refactor sample HomeScreen attachment example * Add buffer license * Remove options store ignored log * Add missing envelope message test and fix breadcrumbs Co-authored-by: Manoel Aranda Neto <[email protected]>
1 parent 42b9f2a commit 396f4f7

File tree

29 files changed

+574
-309
lines changed

29 files changed

+574
-309
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Add attachments support ([#2463](https://github.com/getsentry/sentry-react-native/pull/2463))
8+
39
## 4.3.1
410

511
### Fixes

android/src/main/java/io/sentry/react/RNSentryModule.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.facebook.react.bridge.ReactApplicationContext;
1515
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1616
import com.facebook.react.bridge.ReactMethod;
17+
import com.facebook.react.bridge.ReadableArray;
1718
import com.facebook.react.bridge.ReadableMap;
1819
import com.facebook.react.bridge.ReadableMapKeySetIterator;
1920
import com.facebook.react.bridge.WritableMap;
@@ -292,7 +293,12 @@ public void fetchNativeFrames(Promise promise) {
292293
}
293294

294295
@ReactMethod
295-
public void captureEnvelope(String envelope, Promise promise) {
296+
public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise promise) {
297+
byte bytes[] = new byte[rawBytes.size()];
298+
for (int i = 0; i < rawBytes.size(); i++) {
299+
bytes[i] = (byte) rawBytes.getInt(i);
300+
}
301+
296302
try {
297303
final String outboxPath = HubAdapter.getInstance().getOptions().getOutboxPath();
298304

@@ -302,24 +308,15 @@ public void captureEnvelope(String envelope, Promise promise) {
302308
} else {
303309
File installation = new File(outboxPath, UUID.randomUUID().toString());
304310
try (FileOutputStream out = new FileOutputStream(installation)) {
305-
out.write(envelope.getBytes(Charset.forName("UTF-8")));
311+
out.write(bytes);
306312
}
307313
}
308314
} catch (Throwable ignored) {
309-
logger.severe("Error reading envelope");
315+
logger.severe("Error while writing envelope to outbox.");
310316
}
311317
promise.resolve(true);
312318
}
313319

314-
@ReactMethod
315-
public void getStringBytesLength(String payload, Promise promise) {
316-
try {
317-
promise.resolve(payload.getBytes("UTF-8").length);
318-
} catch (UnsupportedEncodingException e) {
319-
promise.reject(e);
320-
}
321-
}
322-
323320
private static PackageInfo getPackageInfo(Context ctx) {
324321
try {
325322
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);

ios/RNSentry.m

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -270,47 +270,34 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
270270
});
271271
}
272272

273-
RCT_EXPORT_METHOD(captureEnvelope:(NSDictionary * _Nonnull)envelopeDict
273+
RCT_EXPORT_METHOD(captureEnvelope:(NSArray * _Nonnull)bytes
274+
options: (NSDictionary * _Nonnull)options
274275
resolve:(RCTPromiseResolveBlock)resolve
275276
rejecter:(RCTPromiseRejectBlock)reject)
276277
{
277-
if ([NSJSONSerialization isValidJSONObject:envelopeDict]) {
278-
SentrySdkInfo *sdkInfo = [[SentrySdkInfo alloc] initWithDict:envelopeDict[@"header"]];
279-
SentryId *eventId = [[SentryId alloc] initWithUUIDString:envelopeDict[@"header"][@"event_id"]];
280-
SentryTraceContext *traceContext = [[SentryTraceContext alloc] initWithDict:envelopeDict[@"header"][@"trace"]];
281-
SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:eventId sdkInfo:sdkInfo traceContext:traceContext];
282-
283-
NSError *error;
284-
NSData *envelopeItemData = [NSJSONSerialization dataWithJSONObject:envelopeDict[@"payload"] options:0 error:&error];
285-
if (nil != error) {
286-
reject(@"SentryReactNative", @"Cannot serialize event", error);
287-
}
288-
289-
NSString *itemType = envelopeDict[@"payload"][@"type"];
290-
if (itemType == nil) {
291-
// Default to event type.
292-
itemType = @"event";
293-
}
294-
295-
SentryEnvelopeItemHeader *envelopeItemHeader = [[SentryEnvelopeItemHeader alloc] initWithType:itemType length:envelopeItemData.length];
296-
SentryEnvelopeItem *envelopeItem = [[SentryEnvelopeItem alloc] initWithHeader:envelopeItemHeader data:envelopeItemData];
278+
NSMutableData *data = [[NSMutableData alloc] initWithCapacity: [bytes count]];
279+
for(NSNumber *number in bytes) {
280+
char byte = [number charValue];
281+
[data appendBytes: &byte length: 1];
282+
}
297283

298-
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader singleItem:envelopeItem];
284+
SentryEnvelope *envelope = [PrivateSentrySDKOnly envelopeWithData:data];
285+
if (envelope == nil) {
286+
reject(@"SentryReactNative",@"Failed to parse envelope from byte array.", nil);
287+
return;
288+
}
299289

300-
#if DEBUG
290+
#if DEBUG
291+
[PrivateSentrySDKOnly captureEnvelope:envelope];
292+
#else
293+
if (options[@'store']) {
294+
// Storing to disk happens asynchronously with captureEnvelope
295+
[PrivateSentrySDKOnly storeEnvelope:envelope];
296+
} else {
301297
[PrivateSentrySDKOnly captureEnvelope:envelope];
302-
#else
303-
if ([envelopeDict[@"payload"][@"level"] isEqualToString:@"fatal"]) {
304-
// Storing to disk happens asynchronously with captureEnvelope
305-
[PrivateSentrySDKOnly storeEnvelope:envelope];
306-
} else {
307-
[PrivateSentrySDKOnly captureEnvelope:envelope];
308-
}
309-
#endif
310-
resolve(@YES);
311-
} else {
312-
reject(@"SentryReactNative", @"Cannot serialize event", nil);
313-
}
298+
}
299+
#endif
300+
resolve(@YES);
314301
}
315302

316303
RCT_EXPORT_METHOD(setUser:(NSDictionary *)user
1.91 KB
Loading
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.sentry.sample;
2+
import android.content.Context;
3+
4+
import com.facebook.react.bridge.Promise;
5+
import com.facebook.react.bridge.ReactApplicationContext;
6+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
7+
import com.facebook.react.bridge.ReactMethod;
8+
import com.facebook.react.bridge.WritableArray;
9+
import com.facebook.react.bridge.WritableNativeArray;
10+
11+
import java.io.InputStream;
12+
13+
public class AssetsModule extends ReactContextBaseJavaModule {
14+
15+
AssetsModule(ReactApplicationContext context) {
16+
super(context);
17+
}
18+
19+
@Override
20+
public String getName() {
21+
return "AssetsModule";
22+
}
23+
24+
@ReactMethod
25+
public void getExampleAssetData(Promise promise) {
26+
try {
27+
InputStream stream = this.getReactApplicationContext().getResources().getAssets()
28+
.open("logo_mini.png");
29+
int size = stream.available();
30+
byte[] buffer = new byte[size];
31+
stream.read(buffer);
32+
stream.close();
33+
WritableArray array = new WritableNativeArray();
34+
for (int i = 0; i < size; i++) {
35+
array.pushInt(buffer[i]);
36+
}
37+
promise.resolve(array);
38+
} catch (Exception e) {
39+
promise.reject(e);
40+
}
41+
}
42+
}

sample/android/app/src/main/java/io/sentry/sample/MainApplication.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public boolean getUseDeveloperSupport() {
2525
protected List<ReactPackage> getPackages() {
2626
@SuppressWarnings("UnnecessaryLocalVariable")
2727
List<ReactPackage> packages = new PackageList(this).getPackages();
28+
packages.add(new SamplePackage());
2829

2930
for (ReactPackage pkg : packages) {
3031
if (pkg instanceof RNSentryPackage) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.sentry.sample;
2+
import com.facebook.react.ReactPackage;
3+
import com.facebook.react.bridge.NativeModule;
4+
import com.facebook.react.bridge.ReactApplicationContext;
5+
import com.facebook.react.uimanager.ViewManager;
6+
7+
import java.util.ArrayList;
8+
import java.util.Collections;
9+
import java.util.List;
10+
11+
public class SamplePackage implements ReactPackage {
12+
13+
@Override
14+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
15+
return Collections.emptyList();
16+
}
17+
18+
@Override
19+
public List<NativeModule> createNativeModules(
20+
ReactApplicationContext reactContext) {
21+
List<NativeModule> modules = new ArrayList<>();
22+
23+
modules.add(new AssetsModule(reactContext));
24+
25+
return modules;
26+
}
27+
28+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"info" : {
3-
"version" : 1,
4-
"author" : "xcode"
3+
"author" : "xcode",
4+
"version" : 1
55
}
66
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"data" : [
3+
{
4+
"compression-type" : "none",
5+
"filename" : "logo_mini.png",
6+
"idiom" : "universal",
7+
"universal-type-identifier" : "image\/png"
8+
}
9+
],
10+
"info" : {
11+
"author" : "xcode",
12+
"version" : 1
13+
}
14+
}
Loading

sample/ios/sample/RCTAssetsModule.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#import <React/RCTBridgeModule.h>
2+
@interface RCTAssetsModule : NSObject <RCTBridgeModule>
3+
@end

sample/ios/sample/RCTAssetsModule.m

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#import "RCTAssetsModule.h"
2+
3+
@implementation RCTAssetsModule
4+
5+
RCT_EXPORT_METHOD(getExampleAssetData: (RCTPromiseResolveBlock)resolve
6+
rejecter:(RCTPromiseRejectBlock)reject)
7+
{
8+
NSDataAsset *data = [[NSDataAsset alloc] initWithName:@"ExampleBinaryData"];
9+
if (data == nil) {
10+
reject(@"SampleSentryReactNative",@"Failed to load exmaple binary data asset.", nil);
11+
}
12+
13+
NSMutableArray *array = [NSMutableArray arrayWithCapacity:data.data.length];
14+
15+
const char *bytes = [data.data bytes];
16+
17+
for (int i = 0; i < [data.data length]; i++)
18+
{
19+
[array addObject:[[NSNumber alloc] initWithChar:bytes[i]]];
20+
}
21+
22+
resolve(array);
23+
}
24+
25+
RCT_EXPORT_MODULE(AssetsModule);
26+
27+
@end
28+

sample/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ Sentry.init({
2727
// Replace the example DSN below with your own DSN:
2828
dsn: SENTRY_INTERNAL_DSN,
2929
debug: true,
30-
beforeSend: (e) => {
31-
console.log('Event beforeSend:', e);
30+
beforeSend: (e, hint) => {
31+
console.log('Event beforeSend:', e, 'hint:', hint);
3232
return e;
3333
},
3434
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.

sample/src/screens/HomeScreen.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, {useEffect} from 'react';
22
import {
33
Image,
44
ScrollView,
@@ -16,6 +16,10 @@ import * as Sentry from '@sentry/react-native';
1616
import { getTestProps } from '../../utils/getTestProps';
1717
import { SENTRY_INTERNAL_DSN } from '../dsn';
1818
import { SeverityLevel } from '@sentry/types';
19+
import { Scope } from '@sentry/react-native';
20+
import { NativeModules } from 'react-native';
21+
22+
const {AssetsModule} = NativeModules;
1923

2024
interface Props {
2125
navigation: StackNavigationProp<any, 'HomeScreen'>;
@@ -106,6 +110,12 @@ const HomeScreen = (props: Props) => {
106110
console.log('Test scope properties were set.');
107111
};
108112

113+
const [data, setData] = React.useState<Uint8Array>(null);
114+
useEffect(() => {
115+
AssetsModule.getExampleAssetData()
116+
.then((asset: number[]) => setData(new Uint8Array(asset)));
117+
}, []);
118+
109119
return (
110120
<>
111121
<StatusBar barStyle="dark-content" />
@@ -223,6 +233,29 @@ const HomeScreen = (props: Props) => {
223233
</Text>
224234
</TouchableOpacity>
225235
</Sentry.ErrorBoundary>
236+
<View style={styles.spacer} />
237+
<TouchableOpacity
238+
onPress={async () => {
239+
Sentry.configureScope((scope: Scope) => {
240+
scope.addAttachment({
241+
data: 'Attachment content',
242+
filename: 'attachment.txt',
243+
});
244+
scope.addAttachment({data: data, filename: 'logo.png'});
245+
console.log('Sentry attachment added.');
246+
});
247+
}}>
248+
<Text style={styles.buttonText}>Add attachment</Text>
249+
</TouchableOpacity>
250+
<View style={styles.spacer} />
251+
<TouchableOpacity
252+
onPress={async () => {
253+
Sentry.configureScope((scope: Scope) => {
254+
console.log(scope.getAttachments());
255+
});
256+
}}>
257+
<Text style={styles.buttonText}>Get attachment</Text>
258+
</TouchableOpacity>
226259
</View>
227260
<View style={styles.buttonArea}>
228261
<TouchableOpacity

src/js/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
4848
this._browserClient = new BrowserClient({
4949
dsn: options.dsn,
5050
transport: options.transport,
51+
transportOptions: options.transportOptions,
5152
stackParser: options.stackParser || defaultStackParser,
5253
integrations: [],
5354
});

src/js/definitions.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,10 @@ export interface SentryNativeBridgeModule {
3333

3434
addBreadcrumb(breadcrumb: Breadcrumb): void;
3535
captureEnvelope(
36-
payload:
37-
| string
38-
| {
39-
header: Record<string, unknown>;
40-
payload: Record<string, unknown>;
41-
}
36+
bytes: number[],
37+
options: {
38+
store: boolean,
39+
},
4240
): PromiseLike<boolean>;
4341
clearBreadcrumbs(): void;
4442
crash(): void;
@@ -53,7 +51,6 @@ export interface SentryNativeBridgeModule {
5351
fetchNativeDeviceContexts(): PromiseLike<NativeDeviceContextsResponse>;
5452
fetchNativeAppStart(): PromiseLike<NativeAppStartResponse | null>;
5553
fetchNativeFrames(): PromiseLike<NativeFramesResponse | null>;
56-
getStringBytesLength(str: string): Promise<number>;
5754
initNativeSdk(options: ReactNativeOptions): Promise<boolean>;
5855
setUser(
5956
defaultUserKeys: SerializedObject | null,

src/js/integrations/reactnativeerrorhandlers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ export class ReactNativeErrorHandlers implements Integration {
185185

186186
const currentHub = getCurrentHub();
187187
const client = currentHub.getClient<ReactNativeClient>();
188+
const scope = currentHub.getScope();
188189

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

203204
const event = await client.eventFromException(error, {
204-
originalException: error
205+
originalException: error,
206+
attachments: scope?.getAttachments(),
205207
});
206208

207209
if (isFatal) {

0 commit comments

Comments
 (0)