-
-
Notifications
You must be signed in to change notification settings - Fork 427
/
SentryEnvelopeItem.java
316 lines (275 loc) · 12.2 KB
/
SentryEnvelopeItem.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package io.sentry;
import static io.sentry.vendor.Base64.NO_PADDING;
import static io.sentry.vendor.Base64.NO_WRAP;
import io.sentry.exception.SentryEnvelopeException;
import io.sentry.protocol.SentryTransaction;
import io.sentry.util.Objects;
import io.sentry.vendor.Base64;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.concurrent.Callable;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ApiStatus.Internal
public final class SentryEnvelopeItem {
@SuppressWarnings("CharsetObjectCanBeUsed")
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final SentryEnvelopeItemHeader header;
// Either dataFactory is set or data needs to be set.
private final @Nullable Callable<byte[]> dataFactory;
// TODO: Can we have a slice or a reader here instead?
private @Nullable byte[] data;
SentryEnvelopeItem(final @NotNull SentryEnvelopeItemHeader header, final byte[] data) {
this.header = Objects.requireNonNull(header, "SentryEnvelopeItemHeader is required.");
this.data = data;
this.dataFactory = null;
}
SentryEnvelopeItem(
final @NotNull SentryEnvelopeItemHeader header,
final @Nullable Callable<byte[]> dataFactory) {
this.header = Objects.requireNonNull(header, "SentryEnvelopeItemHeader is required.");
this.dataFactory = Objects.requireNonNull(dataFactory, "DataFactory is required.");
this.data = null;
}
// TODO: Should be a Stream
// dataFactory is a Callable which returns theoretically a nullable result. Our implementations
// always provide non-null values.
@SuppressWarnings("NullAway")
public @NotNull byte[] getData() throws Exception {
if (data == null && dataFactory != null) {
data = dataFactory.call();
}
return data;
}
public @NotNull SentryEnvelopeItemHeader getHeader() {
return header;
}
public static @NotNull SentryEnvelopeItem fromSession(
final @NotNull ISerializer serializer, final @NotNull Session session) throws IOException {
Objects.requireNonNull(serializer, "ISerializer is required.");
Objects.requireNonNull(session, "Session is required.");
final CachedItem cachedItem =
new CachedItem(
() -> {
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
serializer.serialize(session, writer);
return stream.toByteArray();
}
});
SentryEnvelopeItemHeader itemHeader =
new SentryEnvelopeItemHeader(
SentryItemType.Session, () -> cachedItem.getBytes().length, "application/json", null);
// Don't use method reference. This can cause issues on Android
return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes());
}
public @Nullable SentryEvent getEvent(final @NotNull ISerializer serializer) throws Exception {
if (header == null || header.getType() != SentryItemType.Event) {
return null;
}
try (final Reader eventReader =
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(getData()), UTF_8))) {
return serializer.deserialize(eventReader, SentryEvent.class);
}
}
public static @NotNull SentryEnvelopeItem fromEvent(
final @NotNull ISerializer serializer, final @NotNull SentryBaseEvent event)
throws IOException {
Objects.requireNonNull(serializer, "ISerializer is required.");
Objects.requireNonNull(event, "SentryEvent is required.");
final CachedItem cachedItem =
new CachedItem(
() -> {
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
serializer.serialize(event, writer);
return stream.toByteArray();
}
});
SentryEnvelopeItemHeader itemHeader =
new SentryEnvelopeItemHeader(
SentryItemType.resolve(event),
() -> cachedItem.getBytes().length,
"application/json",
null);
// Don't use method reference. This can cause issues on Android
return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes());
}
public @Nullable SentryTransaction getTransaction(final @NotNull ISerializer serializer)
throws Exception {
if (header == null || header.getType() != SentryItemType.Transaction) {
return null;
}
try (final Reader eventReader =
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(getData()), UTF_8))) {
return serializer.deserialize(eventReader, SentryTransaction.class);
}
}
public static SentryEnvelopeItem fromUserFeedback(
final @NotNull ISerializer serializer, final @NotNull UserFeedback userFeedback) {
Objects.requireNonNull(serializer, "ISerializer is required.");
Objects.requireNonNull(userFeedback, "UserFeedback is required.");
final CachedItem cachedItem =
new CachedItem(
() -> {
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
serializer.serialize(userFeedback, writer);
return stream.toByteArray();
}
});
SentryEnvelopeItemHeader itemHeader =
new SentryEnvelopeItemHeader(
SentryItemType.UserFeedback,
() -> cachedItem.getBytes().length,
"application/json",
null);
// Don't use method reference. This can cause issues on Android
return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes());
}
public static SentryEnvelopeItem fromAttachment(
final @NotNull Attachment attachment, final long maxAttachmentSize) {
final CachedItem cachedItem =
new CachedItem(
() -> {
if (attachment.getBytes() != null) {
if (attachment.getBytes().length > maxAttachmentSize) {
throw new SentryEnvelopeException(
String.format(
"Dropping attachment with filename '%s', because the "
+ "size of the passed bytes with %d bytes is bigger "
+ "than the maximum allowed attachment size of "
+ "%d bytes.",
attachment.getFilename(),
attachment.getBytes().length,
maxAttachmentSize));
}
return attachment.getBytes();
} else if (attachment.getPathname() != null) {
return readBytesFromFile(attachment.getPathname(), maxAttachmentSize);
}
throw new SentryEnvelopeException(
String.format(
"Couldn't attach the attachment %s.\n"
+ "Please check that either bytes or a path is set.",
attachment.getFilename()));
});
SentryEnvelopeItemHeader itemHeader =
new SentryEnvelopeItemHeader(
SentryItemType.Attachment,
() -> cachedItem.getBytes().length,
attachment.getContentType(),
attachment.getFilename(),
attachment.getAttachmentType());
// Don't use method reference. This can cause issues on Android
return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes());
}
public static @NotNull SentryEnvelopeItem fromProfilingTrace(
final @NotNull ProfilingTraceData profilingTraceData,
final long maxTraceFileSize,
final @NotNull ISerializer serializer)
throws SentryEnvelopeException {
File traceFile = profilingTraceData.getTraceFile();
// Using CachedItem, so we read the trace file in the background
final CachedItem cachedItem =
new CachedItem(
() -> {
if (!traceFile.exists()) {
throw new SentryEnvelopeException(
String.format(
"Dropping profiling trace data, because the file '%s' doesn't exists",
traceFile.getName()));
}
// The payload of the profile item is a json including the trace file encoded with
// base64
byte[] traceFileBytes = readBytesFromFile(traceFile.getPath(), maxTraceFileSize);
String base64Trace = Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING);
profilingTraceData.setSampled_profile(base64Trace);
profilingTraceData.readDeviceCpuFrequencies();
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
serializer.serialize(profilingTraceData, writer);
return stream.toByteArray();
} catch (IOException e) {
throw new SentryEnvelopeException(
String.format("Failed to serialize profiling trace data\n%s", e.getMessage()));
} finally {
// In any case we delete the trace file
traceFile.delete();
}
});
SentryEnvelopeItemHeader itemHeader =
new SentryEnvelopeItemHeader(
SentryItemType.Profile,
() -> cachedItem.getBytes().length,
"application-json",
traceFile.getName());
// Don't use method reference. This can cause issues on Android
return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes());
}
private static byte[] readBytesFromFile(String pathname, long maxFileLength)
throws SentryEnvelopeException {
try {
File file = new File(pathname);
if (!file.isFile()) {
throw new SentryEnvelopeException(
String.format(
"Reading the item %s failed, because the file located at the path is not a file.",
pathname));
}
if (!file.canRead()) {
throw new SentryEnvelopeException(
String.format("Reading the item %s failed, because can't read the file.", pathname));
}
if (file.length() > maxFileLength) {
throw new SentryEnvelopeException(
String.format(
"Dropping item, because its size located at '%s' with %d bytes is bigger "
+ "than the maximum allowed size of %d bytes.",
pathname, file.length(), maxFileLength));
}
try (FileInputStream fileInputStream = new FileInputStream(pathname);
BufferedInputStream inputStream = new BufferedInputStream(fileInputStream);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] bytes = new byte[1024];
int length;
int offset = 0;
while ((length = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, offset, length);
}
return outputStream.toByteArray();
}
} catch (IOException | SecurityException exception) {
throw new SentryEnvelopeException(
String.format("Reading the item %s failed.\n%s", pathname, exception.getMessage()));
}
}
private static class CachedItem {
private @Nullable byte[] bytes;
private final @Nullable Callable<byte[]> dataFactory;
public CachedItem(final @Nullable Callable<byte[]> dataFactory) {
this.dataFactory = dataFactory;
}
public @NotNull byte[] getBytes() throws Exception {
if (bytes == null && dataFactory != null) {
bytes = dataFactory.call();
}
return orEmptyArray(bytes);
}
private static @NotNull byte[] orEmptyArray(final @Nullable byte[] bytes) {
return bytes != null ? bytes : new byte[] {};
}
}
}