@logbrew/react-native 0.1.22 → 0.1.23
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.
- package/LogBrewReactNative.podspec +1 -0
- package/README.md +8 -2
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +16 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +5 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +5 -0
- package/index.cjs +27 -5
- package/index.native.js +8 -1
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
- package/ios/LBRNFatalStoreModule.mm +19 -0
- package/package.json +1 -1
- package/resource-fetch.js +5 -463
- package/src/NativeLogBrewFatalStore.ts +1 -0
package/README.md
CHANGED
|
@@ -588,11 +588,17 @@ export function App({ client }) {
|
|
|
588
588
|
|
|
589
589
|
Screen views carry the versioned `screen_view` analytics classification, and explicit product actions carry `interaction`. The SDK uses the app-owned screen name as a bounded surface and does not inspect view hierarchies, selectors, or input values. Caller metadata cannot replace the reserved classification. See the repository [product analytics capture contract](../../docs/product-analytics-contract.md).
|
|
590
590
|
|
|
591
|
-
The package ships a `react-native` entry that
|
|
591
|
+
The package ships a `react-native` entry that binds platform state and linked native services for Metro, while the default server entry accepts runtime dependencies explicitly. That keeps mobile setup explicit without treating a server process as a native runtime.
|
|
592
592
|
|
|
593
593
|
## Trace Propagation
|
|
594
594
|
|
|
595
|
-
Use an active trace when one product operation should connect screen views, logs, handled errors, actions, network milestones, explicit spans, and outbound request headers. `createReactNativeTraceContext()` continues a valid W3C `traceparent` with a fresh local span ID and falls back to a local root when the incoming value is missing or malformed
|
|
595
|
+
Use an active trace when one product operation should connect screen views, logs, handled errors, actions, network milestones, explicit spans, and outbound request headers. `createReactNativeTraceContext()` continues a valid W3C `traceparent` with a fresh local span ID and falls back to a local root when the incoming value is missing or malformed.
|
|
596
|
+
|
|
597
|
+
The React Native entry creates trace and span IDs with the linked platform's
|
|
598
|
+
cryptographic random source, or Expo Crypto in a managed runtime. It fails
|
|
599
|
+
closed when neither source exists and never falls back to `Math.random` or
|
|
600
|
+
predictable bytes. Callers may still inject `randomValues` when deterministic
|
|
601
|
+
input control is required.
|
|
596
602
|
|
|
597
603
|
```js
|
|
598
604
|
import {
|
|
@@ -12,6 +12,7 @@ import java.io.File;
|
|
|
12
12
|
import java.nio.charset.StandardCharsets;
|
|
13
13
|
import java.security.MessageDigest;
|
|
14
14
|
import java.security.NoSuchAlgorithmException;
|
|
15
|
+
import java.security.SecureRandom;
|
|
15
16
|
import java.util.ArrayList;
|
|
16
17
|
import java.util.Arrays;
|
|
17
18
|
import java.util.HashMap;
|
|
@@ -22,6 +23,7 @@ import java.util.Set;
|
|
|
22
23
|
|
|
23
24
|
final class FatalStoreModuleImpl {
|
|
24
25
|
static final String NAME = "LogBrewFatalStore";
|
|
26
|
+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
|
25
27
|
|
|
26
28
|
private static final Set<String> RECORD_KEYS =
|
|
27
29
|
new HashSet<>(
|
|
@@ -51,6 +53,20 @@ final class FatalStoreModuleImpl {
|
|
|
51
53
|
new AndroidParentDirectorySync());
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
String secureRandomHex(double length) {
|
|
57
|
+
Integer byteCount = integer(length);
|
|
58
|
+
if (byteCount == null || byteCount < 1 || byteCount > 64) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
byte[] bytes = new byte[byteCount];
|
|
62
|
+
SECURE_RANDOM.nextBytes(bytes);
|
|
63
|
+
StringBuilder output = new StringBuilder(byteCount * 2);
|
|
64
|
+
for (byte value : bytes) {
|
|
65
|
+
output.append(String.format(java.util.Locale.ROOT, "%02x", value & 0xff));
|
|
66
|
+
}
|
|
67
|
+
return output.toString();
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
WritableMap loadEventRecords(String queueKey) {
|
|
55
71
|
try {
|
|
56
72
|
EventRecordStore eventStore = eventStore(queueKey);
|
|
@@ -17,6 +17,11 @@ final class FatalStoreModule extends NativeLogBrewFatalStoreSpec {
|
|
|
17
17
|
return FatalStoreModuleImpl.NAME;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
@Override
|
|
21
|
+
public String secureRandomHex(double length) {
|
|
22
|
+
return implementation.secureRandomHex(length);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
@Override
|
|
21
26
|
public WritableMap writeFatalRecord(ReadableMap record) {
|
|
22
27
|
return implementation.writeFatalRecord(record);
|
|
@@ -19,6 +19,11 @@ final class FatalStoreModule extends ReactContextBaseJavaModule {
|
|
|
19
19
|
return FatalStoreModuleImpl.NAME;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
23
|
+
public String secureRandomHex(double length) {
|
|
24
|
+
return implementation.secureRandomHex(length);
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
23
28
|
public WritableMap writeFatalRecord(ReadableMap record) {
|
|
24
29
|
return implementation.writeFatalRecord(record);
|
package/index.cjs
CHANGED
|
@@ -16,8 +16,9 @@ const {
|
|
|
16
16
|
} = require("./metadata.cjs");
|
|
17
17
|
|
|
18
18
|
const DEFAULT_SDK_NAME = "logbrew-react-native";
|
|
19
|
-
const DEFAULT_SDK_VERSION = "0.1.
|
|
19
|
+
const DEFAULT_SDK_VERSION = "0.1.23";
|
|
20
20
|
const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
|
|
21
|
+
const NATIVE_RANDOM_HEX = Symbol.for("co.logbrew.react-native.secure-random-hex");
|
|
21
22
|
const MAX_ACTION_NAME_LENGTH = 64;
|
|
22
23
|
const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
|
|
23
24
|
const SCREEN_ACTION_PREFIX = "screen:";
|
|
@@ -913,11 +914,32 @@ function boundedBreadcrumbMessage(value) {
|
|
|
913
914
|
}
|
|
914
915
|
|
|
915
916
|
function defaultRandomValues(length) {
|
|
916
|
-
if (!globalThis.crypto || typeof globalThis.crypto.getRandomValues !== "function") {
|
|
917
|
-
throw new SdkError("configuration_error", "createReactNativeTraceparent requires crypto.getRandomValues or randomValues");
|
|
918
|
-
}
|
|
919
917
|
const bytes = new Uint8Array(length);
|
|
920
|
-
|
|
918
|
+
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
|
919
|
+
return globalThis.crypto.getRandomValues(bytes);
|
|
920
|
+
}
|
|
921
|
+
if (typeof globalThis.expo?.modules?.ExpoCrypto?.getRandomValues === "function") {
|
|
922
|
+
globalThis.expo.modules.ExpoCrypto.getRandomValues(bytes);
|
|
923
|
+
return bytes;
|
|
924
|
+
}
|
|
925
|
+
const nativeRandom = globalThis[NATIVE_RANDOM_HEX];
|
|
926
|
+
if (typeof nativeRandom !== "function") {
|
|
927
|
+
throw secureRandomError();
|
|
928
|
+
}
|
|
929
|
+
let nativeHex;
|
|
930
|
+
try {
|
|
931
|
+
nativeHex = nativeRandom(length);
|
|
932
|
+
} catch {
|
|
933
|
+
throw secureRandomError();
|
|
934
|
+
}
|
|
935
|
+
if (typeof nativeHex !== "string" || nativeHex.length !== length * 2 || !/^[0-9a-f]+$/iu.test(nativeHex)) {
|
|
936
|
+
throw secureRandomError();
|
|
937
|
+
}
|
|
938
|
+
return Uint8Array.from({ length }, (_, index) => Number.parseInt(nativeHex.slice(index * 2, index * 2 + 2), 16));
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function secureRandomError() {
|
|
942
|
+
return new SdkError("configuration_error", "createReactNativeTraceparent requires secure random values");
|
|
921
943
|
}
|
|
922
944
|
|
|
923
945
|
function headersWithTraceparent(headers, traceparent) {
|
package/index.native.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppState, Platform } from "react-native";
|
|
1
|
+
import { AppState, NativeModules, Platform, TurboModuleRegistry } from "react-native";
|
|
2
2
|
import {
|
|
3
3
|
captureAppStateChange,
|
|
4
4
|
captureReactNativeAction,
|
|
@@ -34,6 +34,13 @@ export {
|
|
|
34
34
|
|
|
35
35
|
export * from "./index.js";
|
|
36
36
|
|
|
37
|
+
const nativeRuntime = TurboModuleRegistry?.get?.("LogBrewFatalStore")
|
|
38
|
+
?? NativeModules?.LogBrewFatalStore;
|
|
39
|
+
if (typeof nativeRuntime?.secureRandomHex === "function") {
|
|
40
|
+
globalThis[Symbol.for("co.logbrew.react-native.secure-random-hex")] =
|
|
41
|
+
nativeRuntime.secureRandomHex.bind(nativeRuntime);
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
export function createLogBrewReactNativeClient(config = {}) {
|
|
38
45
|
const input = config !== null && typeof config === "object" ? config : {};
|
|
39
46
|
const {
|
|
@@ -3,7 +3,7 @@ import Foundation
|
|
|
3
3
|
@objc(LBRNAppleNativeDiagnostics)
|
|
4
4
|
public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
5
5
|
private static let shared = LBRNAppleNativeDiagnostics()
|
|
6
|
-
private static let sdkVersion = "0.1.
|
|
6
|
+
private static let sdkVersion = "0.1.23"
|
|
7
7
|
|
|
8
8
|
private let lock = NSLock()
|
|
9
9
|
private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
#import "LBRNPrivateStorage.h"
|
|
6
6
|
|
|
7
7
|
#import <CommonCrypto/CommonDigest.h>
|
|
8
|
+
#import <Security/SecRandom.h>
|
|
8
9
|
#import <TargetConditionals.h>
|
|
10
|
+
#import <math.h>
|
|
9
11
|
|
|
10
12
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
11
13
|
#import <LogBrewReactNativeSpec/LogBrewReactNativeSpec.h>
|
|
@@ -130,6 +132,23 @@ RCT_EXPORT_MODULE(LogBrewFatalStore)
|
|
|
130
132
|
return self;
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(secureRandomHex:(double)length)
|
|
136
|
+
{
|
|
137
|
+
if (!isfinite(length) || length < 1 || length > 64 || floor(length) != length) {
|
|
138
|
+
return @"";
|
|
139
|
+
}
|
|
140
|
+
uint8_t bytes[64];
|
|
141
|
+
NSUInteger byteCount = (NSUInteger)length;
|
|
142
|
+
if (SecRandomCopyBytes(kSecRandomDefault, byteCount, bytes) != errSecSuccess) {
|
|
143
|
+
return @"";
|
|
144
|
+
}
|
|
145
|
+
NSMutableString *output = [NSMutableString stringWithCapacity:byteCount * 2];
|
|
146
|
+
for (NSUInteger index = 0; index < byteCount; index += 1) {
|
|
147
|
+
[output appendFormat:@"%02x", bytes[index]];
|
|
148
|
+
}
|
|
149
|
+
return output;
|
|
150
|
+
}
|
|
151
|
+
|
|
133
152
|
- (nullable LBRNEventRecordStore *)eventStoreForQueueKey:(NSString *)queueKey
|
|
134
153
|
{
|
|
135
154
|
NSString *queueHash = LBRNQueueHash(queueKey);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logbrew/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "React Native offline delivery, Apple native diagnostics, screen, error, trace, action, and network timeline helpers for LogBrew.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
package/resource-fetch.js
CHANGED
|
@@ -1,464 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
SdkError
|
|
3
|
-
} from "@logbrew/sdk";
|
|
4
|
-
import {
|
|
5
|
-
captureReactNativeResourceSpan,
|
|
6
|
-
createReactNativeTraceContext,
|
|
7
|
-
createTraceparentFetch,
|
|
8
|
-
getActiveLogBrewTrace
|
|
9
|
-
} from "./index.js";
|
|
10
|
-
import {
|
|
11
|
-
createSafeReactNativeMetadata,
|
|
12
|
-
safeReactNativeMetadataFactoryResult
|
|
13
|
-
} from "./metadata.js";
|
|
1
|
+
import implementation from "./resource-fetch.cjs";
|
|
14
2
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function createReactNativeResourceFetch(client, {
|
|
21
|
-
appState,
|
|
22
|
-
fetchImpl,
|
|
23
|
-
metadata = {},
|
|
24
|
-
metadataFactory,
|
|
25
|
-
measureResponseBodySize = false,
|
|
26
|
-
now = () => new Date().toISOString(),
|
|
27
|
-
nowMs = () => Date.now(),
|
|
28
|
-
platform,
|
|
29
|
-
randomValues,
|
|
30
|
-
routeTemplate,
|
|
31
|
-
routeTemplateFactory = defaultRouteTemplateFactory,
|
|
32
|
-
screen,
|
|
33
|
-
sessionId,
|
|
34
|
-
trace,
|
|
35
|
-
traceFlags = "01",
|
|
36
|
-
tracePropagationTargets = []
|
|
37
|
-
} = {}) {
|
|
38
|
-
if (routeTemplateFactory !== undefined && typeof routeTemplateFactory !== "function") {
|
|
39
|
-
throw new SdkError("configuration_error", "routeTemplateFactory must be a function");
|
|
40
|
-
}
|
|
41
|
-
if (metadataFactory !== undefined && typeof metadataFactory !== "function") {
|
|
42
|
-
throw new SdkError("configuration_error", "metadataFactory must be a function");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return async function logBrewResourceFetch(input, init) {
|
|
46
|
-
const startedAtMs = nowMs();
|
|
47
|
-
const timestamp = now();
|
|
48
|
-
const activeTrace = resourceTraceContext({ randomValues, trace, traceFlags });
|
|
49
|
-
const tracedFetch = createTraceparentFetch({
|
|
50
|
-
fetchImpl,
|
|
51
|
-
trace: activeTrace,
|
|
52
|
-
tracePropagationTargets
|
|
53
|
-
});
|
|
54
|
-
const method = requestMethod(input, init);
|
|
55
|
-
const url = requestUrl(input);
|
|
56
|
-
const safeRouteTemplate = routeTemplate ?? routeTemplateFactory({ input, init, url });
|
|
57
|
-
try {
|
|
58
|
-
const response = await tracedFetch(input, init);
|
|
59
|
-
const responseStartDurationMs = elapsedMs(startedAtMs, nowMs);
|
|
60
|
-
const statusCode = responseStatusCode(response);
|
|
61
|
-
const responseSizeBytes = await responseSizeBytesFromResponse(response, { measureResponseBodySize });
|
|
62
|
-
const durationMs = elapsedMs(startedAtMs, nowMs);
|
|
63
|
-
captureReactNativeResourceSpan(client, {
|
|
64
|
-
appState,
|
|
65
|
-
durationMs,
|
|
66
|
-
metadata: {
|
|
67
|
-
...createSafeReactNativeMetadata(metadata, metadataFactory, {
|
|
68
|
-
durationMs,
|
|
69
|
-
init,
|
|
70
|
-
input,
|
|
71
|
-
method,
|
|
72
|
-
response,
|
|
73
|
-
responseSizeBytes,
|
|
74
|
-
responseStartDurationMs,
|
|
75
|
-
routeTemplate: safeRouteTemplate,
|
|
76
|
-
statusCode,
|
|
77
|
-
url
|
|
78
|
-
}),
|
|
79
|
-
responseStartDurationMs
|
|
80
|
-
},
|
|
81
|
-
method,
|
|
82
|
-
platform,
|
|
83
|
-
routeTemplate: safeRouteTemplate,
|
|
84
|
-
screen,
|
|
85
|
-
sessionId,
|
|
86
|
-
responseSizeBytes,
|
|
87
|
-
statusCode,
|
|
88
|
-
timestamp,
|
|
89
|
-
trace: activeTrace
|
|
90
|
-
});
|
|
91
|
-
return response;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
const durationMs = elapsedMs(startedAtMs, nowMs);
|
|
94
|
-
captureReactNativeResourceSpan(client, {
|
|
95
|
-
appState,
|
|
96
|
-
durationMs,
|
|
97
|
-
metadata: {
|
|
98
|
-
...createSafeReactNativeMetadata(metadata, metadataFactory, {
|
|
99
|
-
durationMs,
|
|
100
|
-
error,
|
|
101
|
-
init,
|
|
102
|
-
input,
|
|
103
|
-
method,
|
|
104
|
-
routeTemplate: safeRouteTemplate,
|
|
105
|
-
status: "error",
|
|
106
|
-
url
|
|
107
|
-
}),
|
|
108
|
-
fetchErrorName: errorName(error),
|
|
109
|
-
fetchErrorValueType: typeof error
|
|
110
|
-
},
|
|
111
|
-
method,
|
|
112
|
-
platform,
|
|
113
|
-
routeTemplate: safeRouteTemplate,
|
|
114
|
-
screen,
|
|
115
|
-
sessionId,
|
|
116
|
-
status: "error",
|
|
117
|
-
timestamp,
|
|
118
|
-
trace: activeTrace
|
|
119
|
-
});
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function createReactNativeGraphQLMetadataFactory({
|
|
126
|
-
endpoint,
|
|
127
|
-
metadataFactory
|
|
128
|
-
} = {}) {
|
|
129
|
-
const endpointMatchers = normalizeGraphQLEndpointMatchers(endpoint);
|
|
130
|
-
if (metadataFactory !== undefined && typeof metadataFactory !== "function") {
|
|
131
|
-
throw new SdkError("configuration_error", "metadataFactory must be a function");
|
|
132
|
-
}
|
|
133
|
-
return function logBrewReactNativeGraphQLMetadata(context) {
|
|
134
|
-
const safeMetadata = safeReactNativeMetadataFactoryResult(typeof metadataFactory === "function" ? metadataFactory(context) : undefined);
|
|
135
|
-
if (endpointMatchers.length > 0 && !matchesGraphQLEndpoint(context, endpointMatchers)) {
|
|
136
|
-
return safeMetadata;
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
...safeMetadata,
|
|
140
|
-
...graphqlMetadataFromContext(context)
|
|
141
|
-
};
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function normalizeGraphQLEndpointMatchers(endpoint) {
|
|
146
|
-
if (endpoint === undefined) {
|
|
147
|
-
return [];
|
|
148
|
-
}
|
|
149
|
-
const endpoints = Array.isArray(endpoint) ? endpoint : [endpoint];
|
|
150
|
-
return endpoints.map((candidate) => {
|
|
151
|
-
if (typeof candidate === "string" || candidate instanceof String) {
|
|
152
|
-
const value = normalizeEndpointString(candidate.toString());
|
|
153
|
-
if (value === "") {
|
|
154
|
-
throw new SdkError("configuration_error", "GraphQL endpoint strings must not be empty");
|
|
155
|
-
}
|
|
156
|
-
return value;
|
|
157
|
-
}
|
|
158
|
-
if (candidate instanceof RegExp || typeof candidate === "function") {
|
|
159
|
-
return candidate;
|
|
160
|
-
}
|
|
161
|
-
throw new SdkError("configuration_error", "GraphQL endpoint must be a string, RegExp, function, or array");
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function matchesGraphQLEndpoint(context, endpointMatchers) {
|
|
166
|
-
const candidates = graphqlEndpointCandidates(context);
|
|
167
|
-
return endpointMatchers.some((matcher) => {
|
|
168
|
-
if (typeof matcher === "string") {
|
|
169
|
-
return candidates.includes(matcher);
|
|
170
|
-
}
|
|
171
|
-
if (matcher instanceof RegExp) {
|
|
172
|
-
return candidates.some((candidate) => endpointRegExpMatches(matcher, candidate));
|
|
173
|
-
}
|
|
174
|
-
return matcher(context) === true;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function endpointRegExpMatches(matcher, candidate) {
|
|
179
|
-
matcher.lastIndex = 0;
|
|
180
|
-
const matched = matcher.test(candidate);
|
|
181
|
-
matcher.lastIndex = 0;
|
|
182
|
-
return matched;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function graphqlEndpointCandidates(context) {
|
|
186
|
-
const candidates = new Set();
|
|
187
|
-
if (typeof context?.routeTemplate === "string" && context.routeTemplate.trim() !== "") {
|
|
188
|
-
candidates.add(context.routeTemplate);
|
|
189
|
-
}
|
|
190
|
-
for (const candidate of endpointCandidatesFromUrl(context?.url)) {
|
|
191
|
-
candidates.add(candidate);
|
|
192
|
-
}
|
|
193
|
-
return Array.from(candidates);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function normalizeEndpointString(endpoint) {
|
|
197
|
-
const value = endpoint.trim();
|
|
198
|
-
const candidates = endpointCandidatesFromUrl(value);
|
|
199
|
-
return candidates[candidates.length - 1] ?? value.split(/[?#]/u, 1)[0];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function endpointCandidatesFromUrl(value) {
|
|
203
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
204
|
-
return [];
|
|
205
|
-
}
|
|
206
|
-
const URLConstructor = globalThis.URL;
|
|
207
|
-
if (typeof URLConstructor === "function") {
|
|
208
|
-
try {
|
|
209
|
-
const parsedUrl = new URLConstructor(value, "https://logbrew.local");
|
|
210
|
-
const candidates = [parsedUrl.pathname];
|
|
211
|
-
if (/^https?:\/\//iu.test(value)) {
|
|
212
|
-
candidates.push(`${parsedUrl.origin}${parsedUrl.pathname}`);
|
|
213
|
-
}
|
|
214
|
-
return candidates;
|
|
215
|
-
} catch {
|
|
216
|
-
// Fall back to query/hash stripping below for non-standard request keys.
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return [value.split(/[?#]/u, 1)[0]];
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function graphqlMetadataFromContext(context) {
|
|
223
|
-
const payload = graphqlPayloadFromBody(requestBody(context));
|
|
224
|
-
if (!payload) {
|
|
225
|
-
return {};
|
|
226
|
-
}
|
|
227
|
-
const operation = graphqlOperationDetails(payload.query);
|
|
228
|
-
const operationName = safeGraphqlOperationName(payload.operationName) ?? operation.operationName;
|
|
229
|
-
const metadata = {};
|
|
230
|
-
if (operationName) {
|
|
231
|
-
metadata.graphqlOperationName = operationName;
|
|
232
|
-
}
|
|
233
|
-
if (operation.operationType) {
|
|
234
|
-
metadata.graphqlOperationType = operation.operationType;
|
|
235
|
-
}
|
|
236
|
-
return metadata;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function requestBody(context) {
|
|
240
|
-
const body = context?.init?.body ?? context?.input?.body;
|
|
241
|
-
if (typeof body === "string") {
|
|
242
|
-
return body;
|
|
243
|
-
}
|
|
244
|
-
if (body instanceof String) {
|
|
245
|
-
return body.toString();
|
|
246
|
-
}
|
|
247
|
-
return undefined;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function graphqlPayloadFromBody(body) {
|
|
251
|
-
if (typeof body !== "string" || body.length > MAX_GRAPHQL_BODY_CHARS) {
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
try {
|
|
255
|
-
const payload = JSON.parse(body);
|
|
256
|
-
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : undefined;
|
|
257
|
-
} catch {
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function graphqlOperationDetails(query) {
|
|
263
|
-
if (typeof query !== "string" || query.length > MAX_GRAPHQL_BODY_CHARS) {
|
|
264
|
-
return {};
|
|
265
|
-
}
|
|
266
|
-
const source = query.trimStart();
|
|
267
|
-
if (source.startsWith("{")) {
|
|
268
|
-
return { operationType: "query" };
|
|
269
|
-
}
|
|
270
|
-
const match = GRAPHQL_OPERATION_RE.exec(stripLeadingGraphQLComments(source));
|
|
271
|
-
if (!match) {
|
|
272
|
-
return {};
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
operationName: safeGraphqlOperationName(match[2]),
|
|
276
|
-
operationType: match[1]
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function stripLeadingGraphQLComments(source) {
|
|
281
|
-
return source.replace(/^(?:#[^\n\r]*(?:\r?\n|$)\s*)+/u, "").trimStart();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function safeGraphqlOperationName(value) {
|
|
285
|
-
if (typeof value !== "string") {
|
|
286
|
-
return undefined;
|
|
287
|
-
}
|
|
288
|
-
const name = value.trim();
|
|
289
|
-
if (
|
|
290
|
-
name.length === 0 ||
|
|
291
|
-
name.length > MAX_GRAPHQL_OPERATION_NAME_CHARS ||
|
|
292
|
-
!GRAPHQL_OPERATION_NAME_RE.test(name)
|
|
293
|
-
) {
|
|
294
|
-
return undefined;
|
|
295
|
-
}
|
|
296
|
-
return name;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function requestMethod(input, init) {
|
|
300
|
-
const method = init?.method ?? input?.method ?? "GET";
|
|
301
|
-
return String(method).toUpperCase();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function resourceTraceContext({ randomValues, trace, traceFlags }) {
|
|
305
|
-
if (typeof trace === "string") {
|
|
306
|
-
return createReactNativeTraceContext({ randomValues, traceFlags, traceparent: trace });
|
|
307
|
-
}
|
|
308
|
-
return trace ?? getActiveLogBrewTrace() ?? createReactNativeTraceContext({ randomValues, traceFlags });
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function requestUrl(input) {
|
|
312
|
-
if (typeof input === "string") {
|
|
313
|
-
return input;
|
|
314
|
-
}
|
|
315
|
-
const URLConstructor = globalThis.URL;
|
|
316
|
-
if (typeof URLConstructor === "function" && input instanceof URLConstructor) {
|
|
317
|
-
return input.toString();
|
|
318
|
-
}
|
|
319
|
-
if (typeof input?.url === "string") {
|
|
320
|
-
return input.url;
|
|
321
|
-
}
|
|
322
|
-
return String(input);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function defaultRouteTemplateFactory({ url }) {
|
|
326
|
-
const URLConstructor = globalThis.URL;
|
|
327
|
-
if (typeof URLConstructor === "function") {
|
|
328
|
-
try {
|
|
329
|
-
const parsedUrl = new URLConstructor(url, "https://logbrew.local");
|
|
330
|
-
return parsedUrl.pathname;
|
|
331
|
-
} catch {
|
|
332
|
-
// Fall back to query/hash stripping below for non-standard request keys.
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return String(url).split(/[?#]/u, 1)[0];
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function elapsedMs(startedAtMs, nowMs) {
|
|
339
|
-
const durationMs = nowMs() - startedAtMs;
|
|
340
|
-
return Number.isFinite(durationMs) ? Math.max(0, durationMs) : undefined;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function responseStatusCode(response) {
|
|
344
|
-
return typeof response?.status === "number" && Number.isFinite(response.status) ? response.status : undefined;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function responseSizeBytesFromResponse(response, { measureResponseBodySize = false } = {}) {
|
|
348
|
-
const contentLength = responseContentLengthBytes(response);
|
|
349
|
-
if (contentLength !== undefined) {
|
|
350
|
-
return contentLength;
|
|
351
|
-
}
|
|
352
|
-
return measureResponseBodySize ? clonedResponseBodySizeBytes(response) : undefined;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function responseContentLengthBytes(response) {
|
|
356
|
-
const header = responseHeader(response, "Content-Length");
|
|
357
|
-
const value = Number.parseInt(String(header ?? ""), 10);
|
|
358
|
-
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function responseHeader(response, name) {
|
|
362
|
-
if (typeof response?.headers?.get !== "function") {
|
|
363
|
-
return undefined;
|
|
364
|
-
}
|
|
365
|
-
try {
|
|
366
|
-
return response.headers.get(name) ?? response.headers.get(name.toLowerCase()) ?? undefined;
|
|
367
|
-
} catch {
|
|
368
|
-
return undefined;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async function clonedResponseBodySizeBytes(response) {
|
|
373
|
-
if (typeof response?.clone !== "function") {
|
|
374
|
-
return undefined;
|
|
375
|
-
}
|
|
376
|
-
let clonedResponse;
|
|
377
|
-
try {
|
|
378
|
-
clonedResponse = response.clone();
|
|
379
|
-
} catch {
|
|
380
|
-
return undefined;
|
|
381
|
-
}
|
|
382
|
-
return responseBodySizeBytes(clonedResponse);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async function responseBodySizeBytes(response) {
|
|
386
|
-
const arrayBufferSize = await responseArrayBufferSizeBytes(response);
|
|
387
|
-
if (arrayBufferSize !== undefined) {
|
|
388
|
-
return arrayBufferSize;
|
|
389
|
-
}
|
|
390
|
-
const blobSize = await responseBlobSizeBytes(response);
|
|
391
|
-
if (blobSize !== undefined) {
|
|
392
|
-
return blobSize;
|
|
393
|
-
}
|
|
394
|
-
return responseTextSizeBytes(response);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function responseArrayBufferSizeBytes(response) {
|
|
398
|
-
if (typeof response?.arrayBuffer !== "function") {
|
|
399
|
-
return undefined;
|
|
400
|
-
}
|
|
401
|
-
try {
|
|
402
|
-
return binaryByteLength(await response.arrayBuffer());
|
|
403
|
-
} catch {
|
|
404
|
-
return undefined;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
async function responseBlobSizeBytes(response) {
|
|
409
|
-
if (typeof response?.blob !== "function") {
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
412
|
-
try {
|
|
413
|
-
const blob = await response.blob();
|
|
414
|
-
return typeof blob?.size === "number" && Number.isFinite(blob.size) && blob.size >= 0 ? blob.size : undefined;
|
|
415
|
-
} catch {
|
|
416
|
-
return undefined;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async function responseTextSizeBytes(response) {
|
|
421
|
-
if (typeof response?.text !== "function") {
|
|
422
|
-
return undefined;
|
|
423
|
-
}
|
|
424
|
-
try {
|
|
425
|
-
const text = await response.text();
|
|
426
|
-
return typeof text === "string" || text instanceof String ? utf8ByteLength(text.toString()) : undefined;
|
|
427
|
-
} catch {
|
|
428
|
-
return undefined;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function binaryByteLength(value) {
|
|
433
|
-
if (typeof value?.byteLength === "number" && Number.isFinite(value.byteLength) && value.byteLength >= 0) {
|
|
434
|
-
return value.byteLength;
|
|
435
|
-
}
|
|
436
|
-
return undefined;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function utf8ByteLength(value) {
|
|
440
|
-
let bytes = 0;
|
|
441
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
442
|
-
const code = value.charCodeAt(index);
|
|
443
|
-
if (code <= 0x7f) {
|
|
444
|
-
bytes += 1;
|
|
445
|
-
} else if (code <= 0x7ff) {
|
|
446
|
-
bytes += 2;
|
|
447
|
-
} else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
|
|
448
|
-
const next = value.charCodeAt(index + 1);
|
|
449
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
450
|
-
bytes += 4;
|
|
451
|
-
index += 1;
|
|
452
|
-
} else {
|
|
453
|
-
bytes += 3;
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
bytes += 3;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return bytes;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function errorName(error) {
|
|
463
|
-
return typeof error?.name === "string" && error.name.trim() !== "" ? error.name : "Error";
|
|
464
|
-
}
|
|
3
|
+
export const {
|
|
4
|
+
createReactNativeGraphQLMetadataFactory,
|
|
5
|
+
createReactNativeResourceFetch
|
|
6
|
+
} = implementation;
|
|
@@ -2,6 +2,7 @@ import type { CodegenTypes, TurboModule } from "react-native";
|
|
|
2
2
|
import { TurboModuleRegistry } from "react-native";
|
|
3
3
|
|
|
4
4
|
export interface Spec extends TurboModule {
|
|
5
|
+
secureRandomHex(length: number): string;
|
|
5
6
|
writeFatalRecord(record: CodegenTypes.UnsafeObject): CodegenTypes.UnsafeObject;
|
|
6
7
|
readFatalRecord(): CodegenTypes.UnsafeObject;
|
|
7
8
|
acknowledgeFatalRecord(recordId: string): CodegenTypes.UnsafeObject;
|