@logbrew/react-native 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LogBrewReactNative.podspec +1 -0
  2. package/README.md +70 -7
  3. package/android/CMakeLists.txt +7 -0
  4. package/android/build.gradle +12 -0
  5. package/android/src/main/cpp/android_diagnostics.cpp +363 -0
  6. package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
  7. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +363 -0
  8. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
  9. package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
  10. package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
  11. package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +127 -156
  12. package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
  13. package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
  14. package/android-native-diagnostics.d.ts +29 -0
  15. package/android-native-diagnostics.js +176 -0
  16. package/fatal-replay.cjs +35 -3
  17. package/global-errors.d.cts +3 -3
  18. package/global-errors.d.ts +3 -3
  19. package/global-errors.native.js +1 -12
  20. package/index.cjs +27 -5
  21. package/index.native.d.ts +4 -0
  22. package/index.native.js +29 -4
  23. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
  29. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
  30. package/ios/LBRNFatalStoreModule.mm +34 -86
  31. package/package.json +17 -5
  32. package/persistent-delivery.native.js +4 -3
  33. package/resource-fetch.js +5 -463
  34. package/src/NativeLogBrewFatalStore.ts +6 -4
  35. package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
  36. package/ios/LBRNFatalRecordStore.h +0 -25
  37. package/ios/LBRNFatalRecordStore.m +0 -542
@@ -0,0 +1,214 @@
1
+ package co.logbrew.reactnative;
2
+
3
+ import android.app.ActivityManager;
4
+ import android.content.Context;
5
+ import android.os.Handler;
6
+ import android.os.Looper;
7
+ import android.os.SystemClock;
8
+ import java.io.File;
9
+ import java.util.concurrent.Executors;
10
+ import java.util.concurrent.ScheduledExecutorService;
11
+ import java.util.concurrent.TimeUnit;
12
+ import java.util.concurrent.atomic.AtomicBoolean;
13
+ import java.util.concurrent.atomic.AtomicLong;
14
+ import java.util.function.LongConsumer;
15
+
16
+ final class AndroidDiagnosticsRuntime implements Thread.UncaughtExceptionHandler {
17
+ private static final boolean SIGNAL_CAPTURE_AVAILABLE = loadSignalCapture();
18
+
19
+ private final AndroidNativeDiagnostics diagnostics;
20
+ private final Thread.UncaughtExceptionHandler previousHandler;
21
+ private final AndroidNativeDiagnostics.Configuration configuration;
22
+ private final String signalRecordPath;
23
+
24
+ AndroidDiagnosticsRuntime(
25
+ Context context,
26
+ File storageRoot,
27
+ EventRecordStore eventStore,
28
+ AndroidNativeDiagnostics.Configuration configuration) {
29
+ this.configuration = configuration;
30
+ previousHandler = Thread.getDefaultUncaughtExceptionHandler();
31
+ MainThreadScheduler scheduler = new MainThreadScheduler(context);
32
+ AndroidNativeSignalStore signalStore =
33
+ new AndroidNativeSignalStore(new File(storageRoot, "logbrew-android-native-v1.signal"));
34
+ diagnostics =
35
+ new AndroidNativeDiagnostics(
36
+ eventStore,
37
+ signalStore,
38
+ configuration,
39
+ scheduler,
40
+ () -> {},
41
+ this::chain,
42
+ scheduler.mainThread::getStackTrace);
43
+ signalRecordPath = signalStore.path();
44
+ }
45
+
46
+ synchronized String install() {
47
+ if (diagnostics.installed) {
48
+ return "already_installed";
49
+ }
50
+ if (pending() < 0) {
51
+ throw new IllegalStateException("event queue unavailable");
52
+ }
53
+ String status = diagnostics.install();
54
+ Thread.setDefaultUncaughtExceptionHandler(this);
55
+ if (!diagnostics.signalStore.prepare(new AndroidParentDirectorySync())
56
+ || !SIGNAL_CAPTURE_AVAILABLE
57
+ || !nativeInstall(
58
+ signalRecordPath,
59
+ diagnostics.nextNativeEventId(),
60
+ configuration.projectId,
61
+ configuration.release,
62
+ configuration.environment,
63
+ configuration.service)) {
64
+ Thread.setDefaultUncaughtExceptionHandler(previousHandler);
65
+ diagnostics.uninstall();
66
+ throw new IllegalStateException("signal capture unavailable");
67
+ }
68
+ return status;
69
+ }
70
+
71
+ synchronized String uninstall() {
72
+ if (!diagnostics.installed) {
73
+ return "not_installed";
74
+ }
75
+ if (SIGNAL_CAPTURE_AVAILABLE) {
76
+ nativeUninstall();
77
+ }
78
+ if (Thread.getDefaultUncaughtExceptionHandler() == this) {
79
+ Thread.setDefaultUncaughtExceptionHandler(previousHandler);
80
+ }
81
+ diagnostics.uninstall();
82
+ return "uninstalled";
83
+ }
84
+
85
+ synchronized int pending() {
86
+ EventRecordStore.Result result = diagnostics.eventStore.load();
87
+ return "loaded".equals(result.status) ? result.records.size() : -1;
88
+ }
89
+
90
+ synchronized boolean installed() {
91
+ return diagnostics.installed;
92
+ }
93
+
94
+ synchronized boolean matches(
95
+ EventRecordStore eventStore, AndroidNativeDiagnostics.Configuration configuration) {
96
+ return diagnostics.eventStore == eventStore && diagnostics.configurationMatches(configuration);
97
+ }
98
+
99
+ @Override
100
+ public void uncaughtException(Thread thread, Throwable error) {
101
+ diagnostics.handleUncaught(thread, error);
102
+ }
103
+
104
+ private void chain(Thread thread, Throwable error) {
105
+ if (previousHandler != null && previousHandler != this) {
106
+ previousHandler.uncaughtException(thread, error);
107
+ return;
108
+ }
109
+ android.os.Process.killProcess(android.os.Process.myPid());
110
+ System.exit(10);
111
+ }
112
+
113
+ private static boolean loadSignalCapture() {
114
+ try {
115
+ System.loadLibrary("logbrew_android_diagnostics");
116
+ return true;
117
+ } catch (LinkageError error) {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ private static native boolean nativeInstall(
123
+ String recordPath,
124
+ String eventId,
125
+ String projectId,
126
+ String release,
127
+ String environment,
128
+ String service);
129
+
130
+ private static native void nativeUninstall();
131
+
132
+ private static final class MainThreadScheduler implements AndroidNativeDiagnostics.Scheduler {
133
+ private final Context context;
134
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
135
+ private final Thread mainThread = Looper.getMainLooper().getThread();
136
+ private final AtomicLong heartbeatMs = new AtomicLong(SystemClock.elapsedRealtime());
137
+ private final AtomicBoolean reported = new AtomicBoolean();
138
+ private ScheduledExecutorService executor;
139
+
140
+ MainThreadScheduler(Context context) {
141
+ this.context = context.getApplicationContext();
142
+ }
143
+
144
+ @Override
145
+ public synchronized void start(
146
+ long thresholdMs, LongConsumer report) {
147
+ stop();
148
+ heartbeatMs.set(SystemClock.elapsedRealtime());
149
+ mainHandler.post(this::heartbeat);
150
+ executor =
151
+ Executors.newSingleThreadScheduledExecutor(
152
+ runnable -> {
153
+ Thread thread = new Thread(runnable, "LogBrew-ANR-Watchdog");
154
+ thread.setDaemon(true);
155
+ return thread;
156
+ });
157
+ long intervalMs = Math.max(500, thresholdMs / 2);
158
+ executor.scheduleWithFixedDelay(
159
+ () -> {
160
+ long now = SystemClock.elapsedRealtime();
161
+ long elapsedMs = now - heartbeatMs.get();
162
+ boolean debugging = android.os.Debug.isDebuggerConnected()
163
+ || android.os.Debug.waitingForDebugger();
164
+ if (AndroidNativeDiagnostics.shouldReportHang(
165
+ elapsedMs, thresholdMs, debugging, processNotResponding())
166
+ && reported.compareAndSet(false, true)) {
167
+ report.accept(elapsedMs);
168
+ }
169
+ mainHandler.post(this::heartbeat);
170
+ },
171
+ thresholdMs,
172
+ intervalMs,
173
+ TimeUnit.MILLISECONDS);
174
+ }
175
+
176
+ @Override
177
+ public synchronized void stop() {
178
+ if (executor != null) {
179
+ executor.shutdownNow();
180
+ executor = null;
181
+ }
182
+ }
183
+
184
+ private void heartbeat() {
185
+ heartbeatMs.set(SystemClock.elapsedRealtime());
186
+ reported.set(false);
187
+ }
188
+
189
+ private boolean processNotResponding() {
190
+ ActivityManager manager =
191
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
192
+ if (manager == null) {
193
+ return false;
194
+ }
195
+ try {
196
+ java.util.List<ActivityManager.ProcessErrorStateInfo> states =
197
+ manager.getProcessesInErrorState();
198
+ if (states != null) {
199
+ int processId = android.os.Process.myPid();
200
+ for (ActivityManager.ProcessErrorStateInfo state : states) {
201
+ if (state != null
202
+ && state.pid == processId
203
+ && state.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
204
+ return true;
205
+ }
206
+ }
207
+ }
208
+ } catch (RuntimeException ignored) {
209
+ return false;
210
+ }
211
+ return false;
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,363 @@
1
+ package co.logbrew.reactnative;
2
+
3
+ import java.nio.charset.StandardCharsets;
4
+ import java.security.SecureRandom;
5
+ import java.text.SimpleDateFormat;
6
+ import java.util.Date;
7
+ import java.util.Locale;
8
+ import java.util.TimeZone;
9
+ import java.util.concurrent.atomic.AtomicLong;
10
+ import java.util.function.LongConsumer;
11
+ import java.util.function.Supplier;
12
+ import java.util.regex.Pattern;
13
+
14
+ final class AndroidNativeDiagnostics {
15
+ interface Scheduler {
16
+ void start(long thresholdMs, LongConsumer report);
17
+
18
+ void stop();
19
+ }
20
+
21
+ static final class Configuration {
22
+ private static final Pattern PROJECT_ID =
23
+ Pattern.compile("^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$");
24
+ final String projectId;
25
+ final String release;
26
+ final String environment;
27
+ final String service;
28
+ final String operatingSystemVersion;
29
+ final String deviceModel;
30
+ final String architecture;
31
+ final long anrThresholdMs;
32
+
33
+ Configuration(
34
+ String projectId,
35
+ String release,
36
+ String environment,
37
+ String service,
38
+ String operatingSystemVersion,
39
+ String deviceModel,
40
+ String architecture,
41
+ long anrThresholdMs) {
42
+ if (!PROJECT_ID.matcher(projectId).matches()
43
+ || !bounded(release, 256)
44
+ || !bounded(environment, 128)
45
+ || !bounded(service, 128)
46
+ || !bounded(operatingSystemVersion, 128)
47
+ || !bounded(deviceModel, 128)
48
+ || !isArchitecture(architecture)
49
+ || anrThresholdMs < 2_000
50
+ || anrThresholdMs > 60_000) {
51
+ throw new IllegalArgumentException("invalid Android diagnostics configuration");
52
+ }
53
+ this.projectId = projectId;
54
+ this.release = release;
55
+ this.environment = environment;
56
+ this.service = service;
57
+ this.operatingSystemVersion = operatingSystemVersion;
58
+ this.deviceModel = deviceModel;
59
+ this.architecture = architecture;
60
+ this.anrThresholdMs = anrThresholdMs;
61
+ }
62
+
63
+ private static boolean bounded(String value, int maximum) {
64
+ return value != null
65
+ && !value.isEmpty()
66
+ && value.length() <= maximum
67
+ && value.equals(value.trim())
68
+ && value.chars().noneMatch(
69
+ character -> character <= 31 || character >= 127 && character <= 159);
70
+ }
71
+
72
+ private static boolean isArchitecture(String value) {
73
+ return "arm".equals(value)
74
+ || "arm64".equals(value)
75
+ || "x86".equals(value)
76
+ || "x86_64".equals(value);
77
+ }
78
+
79
+ boolean matches(Configuration other) {
80
+ return other != null
81
+ && projectId.equals(other.projectId)
82
+ && release.equals(other.release)
83
+ && environment.equals(other.environment)
84
+ && service.equals(other.service)
85
+ && operatingSystemVersion.equals(other.operatingSystemVersion)
86
+ && deviceModel.equals(other.deviceModel)
87
+ && architecture.equals(other.architecture)
88
+ && anrThresholdMs == other.anrThresholdMs;
89
+ }
90
+ }
91
+
92
+ private static final int MAX_FRAMES = 32;
93
+ private static final AtomicLong SEQUENCE = new AtomicLong();
94
+
95
+ final EventRecordStore eventStore;
96
+ final AndroidNativeSignalStore signalStore;
97
+ private final Configuration configuration;
98
+ private final Scheduler scheduler;
99
+ private final Runnable afterPersist;
100
+ private final Thread.UncaughtExceptionHandler previousHandler;
101
+ private final Supplier<StackTraceElement[]> mainFrames;
102
+ private final String processNonce;
103
+ boolean installed;
104
+
105
+ AndroidNativeDiagnostics(
106
+ EventRecordStore eventStore,
107
+ AndroidNativeSignalStore signalStore,
108
+ Configuration configuration,
109
+ Scheduler scheduler,
110
+ Runnable afterPersist,
111
+ Thread.UncaughtExceptionHandler previousHandler,
112
+ Supplier<StackTraceElement[]> mainFrames) {
113
+ this.eventStore = eventStore;
114
+ this.signalStore = signalStore;
115
+ this.configuration = configuration;
116
+ this.scheduler = scheduler;
117
+ this.afterPersist = afterPersist;
118
+ this.previousHandler = previousHandler;
119
+ this.mainFrames = mainFrames;
120
+ processNonce = randomHex(8);
121
+ }
122
+
123
+ synchronized String install() {
124
+ if (installed) {
125
+ return "already_installed";
126
+ }
127
+ replaySignalRecord();
128
+ scheduler.start(configuration.anrThresholdMs, this::reportHang);
129
+ installed = true;
130
+ return "installed";
131
+ }
132
+
133
+ synchronized String uninstall() {
134
+ if (!installed) {
135
+ return "not_installed";
136
+ }
137
+ installed = false;
138
+ scheduler.stop();
139
+ return "uninstalled";
140
+ }
141
+
142
+ void handleUncaught(Thread thread, Throwable error) {
143
+ try {
144
+ append(javaCrashEvent(error));
145
+ } finally {
146
+ previousHandler.uncaughtException(thread, error);
147
+ }
148
+ }
149
+
150
+ private synchronized void reportHang(long durationMs) {
151
+ if (!installed) {
152
+ return;
153
+ }
154
+ long boundedDuration =
155
+ Math.max(configuration.anrThresholdMs, Math.min(durationMs, 300_000));
156
+ append(hangEvent(mainFrames.get(), boundedDuration));
157
+ }
158
+
159
+ static boolean shouldReportHang(
160
+ long elapsedMs, long thresholdMs, boolean debugging, boolean processNotResponding) {
161
+ return !debugging && processNotResponding && elapsedMs >= thresholdMs;
162
+ }
163
+
164
+ private void replaySignalRecord() {
165
+ AndroidNativeSignalStore.Record record = signalStore.read();
166
+ if (record != null && append(nativeCrashEvent(record))) {
167
+ signalStore.clear();
168
+ }
169
+ }
170
+
171
+ private boolean append(String serializedEvent) {
172
+ int bytes = serializedEvent.getBytes(StandardCharsets.UTF_8).length;
173
+ EventRecordStore.Result result = eventStore.append(serializedEvent, bytes);
174
+ if (!"appended".equals(result.status)) {
175
+ return false;
176
+ }
177
+ afterPersist.run();
178
+ return true;
179
+ }
180
+
181
+ private String javaCrashEvent(Throwable error) {
182
+ StackTraceElement[] frames = error == null ? new StackTraceElement[0] : error.getStackTrace();
183
+ return issueEvent(
184
+ configuration,
185
+ nextId("java"),
186
+ System.currentTimeMillis(),
187
+ "Native application crash",
188
+ "fatal",
189
+ safeSymbol(error == null ? null : error.getClass().getName(), 256, "AndroidJavaCrash"),
190
+ "uncaught_exception",
191
+ 0,
192
+ frames,
193
+ null);
194
+ }
195
+
196
+ private String hangEvent(StackTraceElement[] frames, long durationMs) {
197
+ return issueEvent(
198
+ configuration,
199
+ nextId("hang"),
200
+ System.currentTimeMillis(),
201
+ "Native application hang",
202
+ "error",
203
+ "AndroidAppHang",
204
+ "anr_watchdog",
205
+ durationMs,
206
+ frames,
207
+ null);
208
+ }
209
+
210
+ private String nativeCrashEvent(AndroidNativeSignalStore.Record record) {
211
+ return issueEvent(
212
+ new Configuration(
213
+ record.projectId,
214
+ record.release,
215
+ record.environment,
216
+ record.service,
217
+ configuration.operatingSystemVersion,
218
+ configuration.deviceModel,
219
+ record.architecture,
220
+ configuration.anrThresholdMs),
221
+ record.id,
222
+ record.timestampMs,
223
+ "Native application crash",
224
+ "fatal",
225
+ "AndroidNativeCrash",
226
+ "signal",
227
+ 0,
228
+ new StackTraceElement[0],
229
+ record);
230
+ }
231
+
232
+ private String issueEvent(
233
+ Configuration eventConfiguration,
234
+ String id,
235
+ long timestampMs,
236
+ String title,
237
+ String level,
238
+ String exceptionType,
239
+ String mechanism,
240
+ long durationMs,
241
+ StackTraceElement[] frames,
242
+ AndroidNativeSignalStore.Record nativeFrame) {
243
+ StringBuilder output = new StringBuilder(2048);
244
+ output.append("{\"type\":\"issue\",\"id\":\"").append(id)
245
+ .append("\",\"timestamp\":\"").append(timestamp(timestampMs))
246
+ .append("\",\"attributes\":{\"title\":\"").append(title)
247
+ .append("\",\"level\":\"").append(level)
248
+ .append("\",\"exception\":{\"type\":\"").append(exceptionType)
249
+ .append("\",\"mechanism\":{\"type\":\"").append(mechanism)
250
+ .append("\",\"handled\":false}},\"metadata\":{")
251
+ .append("\"crash.mechanism\":\"").append(mechanism)
252
+ .append("\",\"crash.replayed\":true,\"crash.handled\":false,")
253
+ .append("\"projectId\":\"").append(eventConfiguration.projectId)
254
+ .append("\",\"release\":\"").append(json(eventConfiguration.release))
255
+ .append("\",\"environment\":\"").append(json(eventConfiguration.environment))
256
+ .append("\",\"service\":\"").append(json(eventConfiguration.service)).append('"');
257
+ if (durationMs > 0) {
258
+ output.append(",\"durationMs\":").append(durationMs);
259
+ }
260
+ if (nativeFrame != null) {
261
+ output.append(",\"crash.signal\":").append(nativeFrame.signal);
262
+ }
263
+ output.append('}');
264
+ appendContext(output, eventConfiguration);
265
+ appendJavaFrames(output, frames);
266
+ if (nativeFrame != null) {
267
+ output.append(",\"nativeStackFrames\":[{\"imageUuid\":\"")
268
+ .append(nativeFrame.imageUuid).append("\",\"architecture\":\"")
269
+ .append(nativeFrame.architecture).append("\",\"instructionOffset\":\"")
270
+ .append(nativeFrame.instructionOffset).append("\"}]");
271
+ }
272
+ return output.append("}}").toString();
273
+ }
274
+
275
+ private static void appendJavaFrames(StringBuilder output, StackTraceElement[] frames) {
276
+ output.append(",\"stackFrames\":[");
277
+ int count = 0;
278
+ for (StackTraceElement frame : frames) {
279
+ if (frame == null || frame.getLineNumber() <= 0 || count == MAX_FRAMES) {
280
+ continue;
281
+ }
282
+ String filename = safeFilename(frame.getFileName());
283
+ if (filename == null) {
284
+ continue;
285
+ }
286
+ if (count > 0) {
287
+ output.append(',');
288
+ }
289
+ output.append("{\"filename\":\"").append(json(filename))
290
+ .append("\",\"line\":").append(frame.getLineNumber())
291
+ .append(",\"column\":1,\"function\":\"")
292
+ .append(json(safeSymbol(frame.getMethodName(), 256, "unknown")))
293
+ .append("\",\"module\":\"")
294
+ .append(json(safeSymbol(frame.getClassName(), 512, "unknown"))).append("\"}");
295
+ count += 1;
296
+ }
297
+ output.append(']');
298
+ }
299
+
300
+ private static void appendContext(StringBuilder output, Configuration value) {
301
+ output.append(",\"context\":{\"resource\":{\"service\":{\"name\":\"")
302
+ .append(json(value.service)).append("\"},\"deployment\":{\"environment\":\"")
303
+ .append(json(value.environment)).append("\",\"release\":\"")
304
+ .append(json(value.release)).append("\"},\"operatingSystem\":{\"name\":\"Android\",\"version\":\"")
305
+ .append(json(value.operatingSystemVersion)).append("\"},\"device\":{\"model\":\"")
306
+ .append(json(value.deviceModel)).append("\",\"architecture\":\"")
307
+ .append(value.architecture).append("\"}}}");
308
+ }
309
+
310
+ private String nextId(String kind) {
311
+ return "evt_android_" + kind + '_' + processNonce + '_' + Long.toString(SEQUENCE.incrementAndGet(), 36);
312
+ }
313
+
314
+ String nextNativeEventId() {
315
+ return nextId("native");
316
+ }
317
+
318
+ boolean configurationMatches(Configuration candidate) {
319
+ return configuration.matches(candidate);
320
+ }
321
+
322
+ private static String safeFilename(String value) {
323
+ String safe = safeSymbol(value, 256, null);
324
+ return safe == null || safe.contains("/") || safe.contains("\\") ? null : safe;
325
+ }
326
+
327
+ private static String safeSymbol(String value, int maximum, String fallback) {
328
+ return value == null
329
+ || value.isEmpty()
330
+ || value.length() > maximum
331
+ || value.chars().anyMatch(character -> character <= 31 || character == 127)
332
+ ? fallback
333
+ : value;
334
+ }
335
+
336
+ private static String json(String value) {
337
+ StringBuilder output = new StringBuilder(value.length());
338
+ for (int index = 0; index < value.length(); index += 1) {
339
+ char character = value.charAt(index);
340
+ if (character == '"' || character == '\\') {
341
+ output.append('\\');
342
+ }
343
+ output.append(character);
344
+ }
345
+ return output.toString();
346
+ }
347
+
348
+ private static String timestamp(long timestampMs) {
349
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
350
+ format.setTimeZone(TimeZone.getTimeZone("UTC"));
351
+ return format.format(new Date(timestampMs));
352
+ }
353
+
354
+ private static String randomHex(int bytes) {
355
+ byte[] value = new byte[bytes];
356
+ new SecureRandom().nextBytes(value);
357
+ StringBuilder output = new StringBuilder(bytes * 2);
358
+ for (byte item : value) {
359
+ output.append(String.format(Locale.ROOT, "%02x", item & 0xff));
360
+ }
361
+ return output.toString();
362
+ }
363
+ }