@debugbundle/sdk-react-native 1.3.0 → 3.0.0
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/CHANGELOG.md +22 -2
- package/DebugBundleReactNative.podspec +2 -2
- package/MIGRATION-3.0.md +9 -0
- package/README.md +8 -3
- package/android/build.gradle +3 -3
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +2 -2
- package/app.plugin.js +1 -1
- package/dist/before-send.d.ts.map +1 -1
- package/dist/before-send.js +6 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +238 -61
- package/dist/console.d.ts +4 -2
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +5 -0
- package/dist/privacy-keys.d.ts +3 -0
- package/dist/privacy-keys.d.ts.map +1 -0
- package/dist/privacy-keys.js +35 -0
- package/dist/privacy-telemetry.d.ts +19 -0
- package/dist/privacy-telemetry.d.ts.map +1 -0
- package/dist/privacy-telemetry.js +164 -0
- package/dist/privacy-text.d.ts +3 -0
- package/dist/privacy-text.d.ts.map +1 -0
- package/dist/privacy-text.js +85 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +29 -45
- package/ios/DebugBundleReactNative.swift +2 -2
- package/package.json +6 -5
- package/src/before-send.ts +6 -0
- package/src/client.ts +226 -70
- package/src/console.ts +6 -1
- package/src/privacy-keys.ts +39 -0
- package/src/privacy-telemetry.ts +168 -0
- package/src/privacy-text.ts +86 -0
- package/src/redaction.ts +26 -46
- package/android/src/test/java/com/debugbundle/reactnative/DebugBundleReactNativeModuleTest.java +0 -474
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { DEFAULT_SENSITIVE_KEYS, isSensitiveKey } from "./privacy-keys.js";
|
|
2
|
+
|
|
3
|
+
const REDACTED = "[REDACTED]";
|
|
4
|
+
const BUILTIN_LABELS = [...DEFAULT_SENSITIVE_KEYS, "client_secret", "x_api_key",
|
|
5
|
+
"accessToken", "refreshToken", "privateKey", "clientSecret"];
|
|
6
|
+
const ASSIGNMENT = new RegExp(
|
|
7
|
+
`\\b(${BUILTIN_LABELS.join("|")})\\b(["']?\\s*[:=]\\s*)(?:"[^"]*"|'[^']*'|[^\\s&,;]+)`,
|
|
8
|
+
"gi"
|
|
9
|
+
);
|
|
10
|
+
const HEADER = /\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/gi;
|
|
11
|
+
const PEM = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/gi;
|
|
12
|
+
const PEM_COMPLETE = new RegExp(PEM.source, "i");
|
|
13
|
+
const PEM_START = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
|
|
14
|
+
const DEBUGBUNDLE_TOKEN = /\bdbundle_(?:proj|mem|probe|agent)_[A-Za-z0-9_-]+\b/g;
|
|
15
|
+
const BEARER = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]{6,}/gi;
|
|
16
|
+
const URL_TEXT = /\bhttps?:\/\/[^\s<>"']+/gi;
|
|
17
|
+
// Do not treat numeric substrings inside UUIDs, hashes, or identifier slugs as cards.
|
|
18
|
+
const CARD = /(?<![A-Za-z0-9_-])(?:\d[ -]?){12,18}\d(?![A-Za-z0-9_-])/g;
|
|
19
|
+
|
|
20
|
+
function isValidCard(value: string): boolean {
|
|
21
|
+
const digits = value.replace(/[^0-9]/g, "");
|
|
22
|
+
if (digits.length < 13 || digits.length > 19 || /^(\d)\1+$/.test(digits)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
let sum = 0;
|
|
26
|
+
for (let index = digits.length - 1; index >= 0; index -= 1) {
|
|
27
|
+
let digit = Number(digits[index]);
|
|
28
|
+
if ((digits.length - index) % 2 === 0) {
|
|
29
|
+
digit *= 2;
|
|
30
|
+
if (digit > 9) digit -= 9;
|
|
31
|
+
}
|
|
32
|
+
sum += digit;
|
|
33
|
+
}
|
|
34
|
+
return sum % 10 === 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scrubUrl(raw: string, keys: readonly string[]): string {
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(raw);
|
|
40
|
+
if (url.username || url.password) {
|
|
41
|
+
url.username = "REDACTED";
|
|
42
|
+
url.password = "";
|
|
43
|
+
}
|
|
44
|
+
const nextQuery = new URLSearchParams();
|
|
45
|
+
for (const [key, value] of url.searchParams) {
|
|
46
|
+
if (key.length > 128 || scrubCredentialText(key, keys, false) !== key) continue;
|
|
47
|
+
nextQuery.append(key, isSensitiveKey(key, keys) || scrubCredentialText(value, keys, false) !== value ? REDACTED : value);
|
|
48
|
+
}
|
|
49
|
+
url.search = nextQuery.toString();
|
|
50
|
+
url.hash = "";
|
|
51
|
+
return url.toString();
|
|
52
|
+
} catch {
|
|
53
|
+
// A malformed URL with a recognizable credential must not be emitted verbatim.
|
|
54
|
+
return REDACTED;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Bounded caller input only. Returns stable replacement text, never the matching secret. */
|
|
59
|
+
export function scrubCredentialText(input: string, additionalKeys: readonly string[], scanUrls = true): string {
|
|
60
|
+
const keys = [...DEFAULT_SENSITIVE_KEYS, ...additionalKeys];
|
|
61
|
+
if (PEM_START.test(input) && !PEM_COMPLETE.test(input)) return REDACTED;
|
|
62
|
+
let output = input;
|
|
63
|
+
if (/(?:password|token|secret|authorization|cookie)%3[ad]/i.test(output)) {
|
|
64
|
+
try {
|
|
65
|
+
output = decodeURIComponent(output);
|
|
66
|
+
} catch {
|
|
67
|
+
return REDACTED;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
output = output.replace(PEM, REDACTED).replace(HEADER, "$1: [REDACTED]");
|
|
71
|
+
output = output.replace(BEARER, "$1 [REDACTED]").replace(DEBUGBUNDLE_TOKEN, REDACTED);
|
|
72
|
+
output = output.replace(ASSIGNMENT, "$1$2[REDACTED]");
|
|
73
|
+
if (additionalKeys.length > 0) {
|
|
74
|
+
for (const key of additionalKeys) {
|
|
75
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
76
|
+
const named = new RegExp(`\\b(${escaped})\\b(["']?\\s*[:=]\\s*)(?:"[^"]*"|'[^']*'|[^\\s&,;]+)`, "gi");
|
|
77
|
+
output = output.replace(named, "$1$2[REDACTED]");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
output = output.replace(CARD, (candidate) => isValidCard(candidate) ? REDACTED : candidate);
|
|
81
|
+
if (!scanUrls) return output;
|
|
82
|
+
return output.replace(URL_TEXT, (matched) => {
|
|
83
|
+
const trailing = matched.match(/[).,;]+$/)?.[0] ?? "";
|
|
84
|
+
return scrubUrl(matched.slice(0, matched.length - trailing.length), keys) + trailing;
|
|
85
|
+
});
|
|
86
|
+
}
|
package/src/redaction.ts
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_SENSITIVE_KEYS, isSensitiveKey as privacySensitiveKey } from "./privacy-keys.js";
|
|
2
|
+
import { sanitizeTelemetry } from "./privacy-telemetry.js";
|
|
3
|
+
|
|
1
4
|
const DEFAULT_REDACT_FIELDS = [
|
|
2
|
-
|
|
3
|
-
"secret",
|
|
4
|
-
"token",
|
|
5
|
-
"api_key",
|
|
6
|
-
"apikey",
|
|
7
|
-
"access_token",
|
|
8
|
-
"refresh_token",
|
|
9
|
-
"private_key",
|
|
10
|
-
"passwd",
|
|
11
|
-
"card_number",
|
|
12
|
-
"cvv",
|
|
13
|
-
"cvc",
|
|
14
|
-
"pin",
|
|
15
|
-
"expiry",
|
|
16
|
-
"phone",
|
|
17
|
-
"bearer",
|
|
18
|
-
"session_id",
|
|
19
|
-
"otp",
|
|
20
|
-
"verification_code",
|
|
21
|
-
"authorization",
|
|
22
|
-
"cookie",
|
|
23
|
-
"ssn"
|
|
5
|
+
...DEFAULT_SENSITIVE_KEYS
|
|
24
6
|
];
|
|
25
7
|
|
|
26
8
|
export const DEFAULT_HEADER_ALLOWLIST = [
|
|
@@ -46,8 +28,15 @@ export function defaultRedactFields(): string[] {
|
|
|
46
28
|
}
|
|
47
29
|
|
|
48
30
|
export function sanitizeValue(value: unknown, options: SanitizeOptions = {}): unknown {
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
try {
|
|
32
|
+
const normalized = normalizeOptions(options);
|
|
33
|
+
const visited = new WeakSet<object>();
|
|
34
|
+
const prepared = sanitize(value, normalized, 0, visited, undefined);
|
|
35
|
+
const protectedValue = sanitizeTelemetry(prepared, { additionalKeys: options.redactFields ?? [] });
|
|
36
|
+
return protectedValue.ok ? protectedValue.value : "[REDACTED]";
|
|
37
|
+
} catch {
|
|
38
|
+
return "[REDACTED]";
|
|
39
|
+
}
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
export function sanitizeHeaders(
|
|
@@ -71,7 +60,7 @@ export function sanitizeHeaders(
|
|
|
71
60
|
|
|
72
61
|
function normalizeOptions(options: SanitizeOptions): Required<SanitizeOptions> {
|
|
73
62
|
return {
|
|
74
|
-
redactFields: options.redactFields ??
|
|
63
|
+
redactFields: [...DEFAULT_REDACT_FIELDS, ...(options.redactFields ?? [])],
|
|
75
64
|
maxDepth: options.maxDepth ?? 8,
|
|
76
65
|
maxStringLength: options.maxStringLength ?? 2048,
|
|
77
66
|
maxArrayLength: options.maxArrayLength ?? 50,
|
|
@@ -87,13 +76,13 @@ function sanitize(
|
|
|
87
76
|
key: string | undefined
|
|
88
77
|
): unknown {
|
|
89
78
|
if (key && isSensitiveKey(key, options.redactFields)) {
|
|
90
|
-
return "[
|
|
79
|
+
return "[REDACTED]";
|
|
91
80
|
}
|
|
92
81
|
if (value === null || value === undefined) {
|
|
93
82
|
return value ?? null;
|
|
94
83
|
}
|
|
95
84
|
if (typeof value === "string") {
|
|
96
|
-
return truncate(value, options.maxStringLength);
|
|
85
|
+
return truncate(value, options.maxStringLength, options.redactFields);
|
|
97
86
|
}
|
|
98
87
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
99
88
|
return value;
|
|
@@ -136,30 +125,21 @@ function sanitizeError(
|
|
|
136
125
|
visited: WeakSet<object>
|
|
137
126
|
): Record<string, unknown> {
|
|
138
127
|
return {
|
|
139
|
-
name: truncate(error.name || "Error", options.maxStringLength),
|
|
140
|
-
message: truncate(error.message || "", options.maxStringLength),
|
|
141
|
-
stack: typeof error.stack === "string" ? truncate(error.stack, options.maxStringLength * 4) : null,
|
|
128
|
+
name: truncate(error.name || "Error", options.maxStringLength, options.redactFields),
|
|
129
|
+
message: truncate(error.message || "", options.maxStringLength, options.redactFields),
|
|
130
|
+
stack: typeof error.stack === "string" ? truncate(error.stack, options.maxStringLength * 4, options.redactFields) : null,
|
|
142
131
|
cause: "cause" in error ? sanitize(error.cause, options, depth + 1, visited, "cause") : null
|
|
143
132
|
};
|
|
144
133
|
}
|
|
145
134
|
|
|
146
|
-
function truncate(value: string, maxLength: number): string {
|
|
147
|
-
if (value.length
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
135
|
+
function truncate(value: string, maxLength: number, fields: string[]): string {
|
|
136
|
+
if (value.length > 16 * 1024) return "[REDACTED]";
|
|
137
|
+
const safe = sanitizeTelemetry(value, { additionalKeys: fields });
|
|
138
|
+
if (!safe.ok || typeof safe.value !== "string") return "[REDACTED]";
|
|
139
|
+
if (safe.value.length <= maxLength) return safe.value;
|
|
140
|
+
return `${safe.value.slice(0, maxLength)}...[Truncated]`;
|
|
151
141
|
}
|
|
152
142
|
|
|
153
143
|
function isSensitiveKey(key: string, sensitiveFields: string[]): boolean {
|
|
154
|
-
|
|
155
|
-
const sensitive = new Set(sensitiveFields.flatMap(splitKey));
|
|
156
|
-
return segments.some((segment) => sensitive.has(segment));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function splitKey(key: string): string[] {
|
|
160
|
-
return key
|
|
161
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
162
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
163
|
-
.map((segment) => segment.toLowerCase())
|
|
164
|
-
.filter(Boolean);
|
|
144
|
+
return privacySensitiveKey(key, [...DEFAULT_SENSITIVE_KEYS, ...sensitiveFields]);
|
|
165
145
|
}
|
package/android/src/test/java/com/debugbundle/reactnative/DebugBundleReactNativeModuleTest.java
DELETED
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
package com.debugbundle.reactnative;
|
|
2
|
-
|
|
3
|
-
import static org.junit.Assert.assertEquals;
|
|
4
|
-
import static org.junit.Assert.assertFalse;
|
|
5
|
-
import static org.junit.Assert.assertTrue;
|
|
6
|
-
import static org.mockito.ArgumentMatchers.any;
|
|
7
|
-
import static org.mockito.ArgumentMatchers.anyBoolean;
|
|
8
|
-
import static org.mockito.ArgumentMatchers.anyString;
|
|
9
|
-
import static org.mockito.ArgumentMatchers.eq;
|
|
10
|
-
import static org.mockito.ArgumentMatchers.isNull;
|
|
11
|
-
import static org.mockito.Mockito.doThrow;
|
|
12
|
-
import static org.mockito.Mockito.mock;
|
|
13
|
-
import static org.mockito.Mockito.never;
|
|
14
|
-
import static org.mockito.Mockito.reset;
|
|
15
|
-
import static org.mockito.Mockito.verify;
|
|
16
|
-
import static org.mockito.Mockito.when;
|
|
17
|
-
|
|
18
|
-
import android.content.Context;
|
|
19
|
-
import com.debugbundle.android.DebugBundleConfig;
|
|
20
|
-
import com.debugbundle.android.DebugBundleLogLevel;
|
|
21
|
-
import com.debugbundle.android.DebugBundleRequestInfo;
|
|
22
|
-
import com.debugbundle.android.DebugBundleResponseInfo;
|
|
23
|
-
import com.facebook.react.bridge.Dynamic;
|
|
24
|
-
import com.facebook.react.bridge.Promise;
|
|
25
|
-
import com.facebook.react.bridge.NativeModule;
|
|
26
|
-
import com.facebook.react.bridge.ReactApplicationContext;
|
|
27
|
-
import com.facebook.react.bridge.ReadableArray;
|
|
28
|
-
import com.facebook.react.bridge.ReadableMap;
|
|
29
|
-
import com.facebook.react.bridge.ReadableType;
|
|
30
|
-
import com.facebook.react.bridge.WritableMap;
|
|
31
|
-
import com.facebook.react.uimanager.ViewManager;
|
|
32
|
-
import java.util.HashMap;
|
|
33
|
-
import java.util.List;
|
|
34
|
-
import java.util.Map;
|
|
35
|
-
import org.junit.Before;
|
|
36
|
-
import org.junit.Test;
|
|
37
|
-
import org.mockito.ArgumentCaptor;
|
|
38
|
-
|
|
39
|
-
public final class DebugBundleReactNativeModuleTest {
|
|
40
|
-
private ReactApplicationContext reactContext;
|
|
41
|
-
private Context applicationContext;
|
|
42
|
-
private DebugBundleReactNativeModule.NativeOperations nativeOperations;
|
|
43
|
-
private DebugBundleReactNativeModule.WritableMapFactory writableMapFactory;
|
|
44
|
-
private WritableMap writableMap;
|
|
45
|
-
private DebugBundleReactNativeModule module;
|
|
46
|
-
|
|
47
|
-
@Before
|
|
48
|
-
public void setUp() {
|
|
49
|
-
reactContext = mock(ReactApplicationContext.class);
|
|
50
|
-
applicationContext = mock(Context.class);
|
|
51
|
-
nativeOperations = mock(DebugBundleReactNativeModule.NativeOperations.class);
|
|
52
|
-
writableMapFactory = mock(DebugBundleReactNativeModule.WritableMapFactory.class);
|
|
53
|
-
writableMap = mock(WritableMap.class);
|
|
54
|
-
when(reactContext.getApplicationContext()).thenReturn(applicationContext);
|
|
55
|
-
when(nativeOperations.statusName()).thenReturn("ONLINE");
|
|
56
|
-
when(writableMapFactory.create()).thenReturn(writableMap);
|
|
57
|
-
module = new DebugBundleReactNativeModule(
|
|
58
|
-
reactContext,
|
|
59
|
-
nativeOperations,
|
|
60
|
-
writableMapFactory);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@Test
|
|
64
|
-
public void initializesWithFullConfigurationAndReturnsNativeState() {
|
|
65
|
-
when(nativeOperations.lastEventAt()).thenReturn(1_234L);
|
|
66
|
-
Promise promise = mock(Promise.class);
|
|
67
|
-
ReadableMap config = readableMap(Map.ofEntries(
|
|
68
|
-
Map.entry("sdkVersion", "1.2.7"),
|
|
69
|
-
Map.entry("projectToken", "project-token"),
|
|
70
|
-
Map.entry("enabled", false),
|
|
71
|
-
Map.entry("environment", "staging"),
|
|
72
|
-
Map.entry("service", "checkout-mobile"),
|
|
73
|
-
Map.entry("endpoint", "https://ingest.example.test"),
|
|
74
|
-
Map.entry("batchSize", 4),
|
|
75
|
-
Map.entry("flushInterval", 1_250.0),
|
|
76
|
-
Map.entry("sampleRate", 0.5),
|
|
77
|
-
Map.entry("sessionSampleRate", 0.75),
|
|
78
|
-
Map.entry("requestTimeout", 2_500.0),
|
|
79
|
-
Map.entry("releaseChannel", "beta"),
|
|
80
|
-
Map.entry("appVersion", "4.2.0"),
|
|
81
|
-
Map.entry("buildNumber", "42"),
|
|
82
|
-
Map.entry("maxEventsPerSession", 55),
|
|
83
|
-
Map.entry("maxBreadcrumbs", 12),
|
|
84
|
-
Map.entry("captureScreens", false),
|
|
85
|
-
Map.entry("captureActions", true),
|
|
86
|
-
Map.entry("captureNetwork", false),
|
|
87
|
-
Map.entry("captureLogs", false),
|
|
88
|
-
Map.entry("logLevel", "debug"),
|
|
89
|
-
Map.entry("headerAllowlist", List.of("x-request-id", 4)),
|
|
90
|
-
Map.entry("offlineQueueMaxEvents", 75),
|
|
91
|
-
Map.entry("offlineQueueMaxBytes", 4_096.0),
|
|
92
|
-
Map.entry("offlineQueueTtl", 9_000.0),
|
|
93
|
-
Map.entry("maxProbeLabels", 8),
|
|
94
|
-
Map.entry("maxProbeEntriesPerLabel", 3),
|
|
95
|
-
Map.entry("probeFlushOnError", false),
|
|
96
|
-
Map.entry("redactFields", List.of("password"))));
|
|
97
|
-
|
|
98
|
-
module.initialize(config, promise);
|
|
99
|
-
|
|
100
|
-
verify(nativeOperations).initialize(eq(applicationContext), any(DebugBundleConfig.class));
|
|
101
|
-
verify(writableMap).putString("status", "online");
|
|
102
|
-
verify(writableMap).putDouble("lastEventAt", 1_234.0);
|
|
103
|
-
verify(writableMap).putBoolean("nativeModuleAvailable", true);
|
|
104
|
-
verify(writableMap).putNull("degradedReason");
|
|
105
|
-
verify(promise).resolve(writableMap);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@Test
|
|
109
|
-
public void initializationAndStatusFailOpenWithoutThrowingIntoReactNative() {
|
|
110
|
-
Promise failedPromise = mock(Promise.class);
|
|
111
|
-
doThrow(new IllegalStateException("native failure"))
|
|
112
|
-
.when(nativeOperations)
|
|
113
|
-
.initialize(any(Context.class), any(DebugBundleConfig.class));
|
|
114
|
-
|
|
115
|
-
module.initialize(readableMap(Map.of()), failedPromise);
|
|
116
|
-
|
|
117
|
-
verify(writableMap).putString("status", "degraded");
|
|
118
|
-
verify(writableMap).putNull("lastEventAt");
|
|
119
|
-
verify(writableMap).putString("degradedReason", "android_initialize_failed");
|
|
120
|
-
verify(failedPromise).resolve(writableMap);
|
|
121
|
-
|
|
122
|
-
reset(writableMap);
|
|
123
|
-
when(nativeOperations.lastEventAt()).thenReturn(null);
|
|
124
|
-
Promise statusPromise = mock(Promise.class);
|
|
125
|
-
module.getStatus(statusPromise);
|
|
126
|
-
verify(writableMap).putNull("lastEventAt");
|
|
127
|
-
verify(statusPromise).resolve(writableMap);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@Test
|
|
131
|
-
public void translatesEveryLegacyEventFamilyThroughNativeOperations() {
|
|
132
|
-
assertLegacyEventCalls(
|
|
133
|
-
Map.of(
|
|
134
|
-
"event_type", "frontend_exception",
|
|
135
|
-
"payload", Map.of("error", "checkout failed")),
|
|
136
|
-
() -> verify(nativeOperations).captureException(
|
|
137
|
-
any(RuntimeException.class),
|
|
138
|
-
any(Map.class)));
|
|
139
|
-
assertLegacyEventCalls(
|
|
140
|
-
Map.of(
|
|
141
|
-
"event_type", "frontend_breadcrumb",
|
|
142
|
-
"payload", Map.of("breadcrumb_type", "screen_transition")),
|
|
143
|
-
() -> verify(nativeOperations).captureBreadcrumb(
|
|
144
|
-
eq("screen_transition"),
|
|
145
|
-
isNull(),
|
|
146
|
-
any(Map.class)));
|
|
147
|
-
|
|
148
|
-
for (Map.Entry<String, DebugBundleLogLevel> entry : Map.of(
|
|
149
|
-
"debug", DebugBundleLogLevel.Debug,
|
|
150
|
-
"info", DebugBundleLogLevel.Info,
|
|
151
|
-
"error", DebugBundleLogLevel.Error,
|
|
152
|
-
"critical", DebugBundleLogLevel.Critical,
|
|
153
|
-
"warning", DebugBundleLogLevel.Warning).entrySet()) {
|
|
154
|
-
assertLegacyEventCalls(
|
|
155
|
-
Map.of(
|
|
156
|
-
"event_type", "log_event",
|
|
157
|
-
"payload", Map.of("message", "checkout", "level", entry.getKey())),
|
|
158
|
-
() -> verify(nativeOperations).captureLog(
|
|
159
|
-
eq("checkout"),
|
|
160
|
-
eq(entry.getValue()),
|
|
161
|
-
any(Map.class)));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
assertLegacyEventCalls(
|
|
165
|
-
Map.of(
|
|
166
|
-
"event_type", "request_event",
|
|
167
|
-
"correlation", Map.of("trace_id", "trace-rn"),
|
|
168
|
-
"payload", Map.of(
|
|
169
|
-
"method", "POST",
|
|
170
|
-
"url", "https://example.test/checkout",
|
|
171
|
-
"route_template", "/checkout",
|
|
172
|
-
"response_status", 503,
|
|
173
|
-
"duration_ms", 12.5)),
|
|
174
|
-
() -> verify(nativeOperations).captureRequest(
|
|
175
|
-
any(DebugBundleRequestInfo.class),
|
|
176
|
-
any(DebugBundleResponseInfo.class),
|
|
177
|
-
any(Map.class),
|
|
178
|
-
eq(false)));
|
|
179
|
-
assertLegacyEventCalls(
|
|
180
|
-
Map.of("event_type", "custom_event"),
|
|
181
|
-
() -> verify(nativeOperations).captureMessage(
|
|
182
|
-
eq("react_native_event:custom_event"),
|
|
183
|
-
eq(DebugBundleLogLevel.Warning),
|
|
184
|
-
any(Map.class)));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
@Test
|
|
188
|
-
public void canonicalEventsAndProbeOperationsReturnTruthfulResultsAndFailClosed() {
|
|
189
|
-
ReadableMap event = readableMap(Map.of("event_type", "frontend_exception"));
|
|
190
|
-
Promise canonicalPromise = mock(Promise.class);
|
|
191
|
-
when(nativeOperations.captureExternalEvent(any(Map.class))).thenReturn(true);
|
|
192
|
-
module.enqueueCanonicalEvent(event, canonicalPromise);
|
|
193
|
-
verify(canonicalPromise).resolve(true);
|
|
194
|
-
|
|
195
|
-
when(nativeOperations.isProbeActive("checkout.*")).thenReturn(true);
|
|
196
|
-
assertTrue(module.isProbeActive("checkout.*"));
|
|
197
|
-
doThrow(new IllegalStateException("probe failure"))
|
|
198
|
-
.when(nativeOperations)
|
|
199
|
-
.isProbeActive("broken");
|
|
200
|
-
assertFalse(module.isProbeActive("broken"));
|
|
201
|
-
|
|
202
|
-
Promise initializePromise = mock(Promise.class);
|
|
203
|
-
module.initialize(
|
|
204
|
-
readableMap(Map.of(
|
|
205
|
-
"sdkVersion", "1.2.7",
|
|
206
|
-
"service", "checkout-mobile",
|
|
207
|
-
"environment", "test")),
|
|
208
|
-
initializePromise);
|
|
209
|
-
Promise probePromise = mock(Promise.class);
|
|
210
|
-
when(nativeOperations.captureExternalProbe(
|
|
211
|
-
anyString(),
|
|
212
|
-
anyString(),
|
|
213
|
-
anyString(),
|
|
214
|
-
anyString(),
|
|
215
|
-
any(Map.class),
|
|
216
|
-
anyString())).thenReturn(true);
|
|
217
|
-
module.captureProbe(
|
|
218
|
-
"checkout.cart",
|
|
219
|
-
readableMap(Map.of("count", 2)),
|
|
220
|
-
"2026-07-28T12:00:00.000Z",
|
|
221
|
-
probePromise);
|
|
222
|
-
verify(nativeOperations).captureExternalProbe(
|
|
223
|
-
"1.2.7",
|
|
224
|
-
"checkout-mobile",
|
|
225
|
-
"test",
|
|
226
|
-
"checkout.cart",
|
|
227
|
-
Map.of("count", 2),
|
|
228
|
-
"2026-07-28T12:00:00.000Z");
|
|
229
|
-
verify(probePromise).resolve(true);
|
|
230
|
-
|
|
231
|
-
Promise failedCanonicalPromise = mock(Promise.class);
|
|
232
|
-
doThrow(new IllegalStateException("capture failure"))
|
|
233
|
-
.when(nativeOperations)
|
|
234
|
-
.captureExternalEvent(any(Map.class));
|
|
235
|
-
module.enqueueCanonicalEvent(event, failedCanonicalPromise);
|
|
236
|
-
verify(failedCanonicalPromise).resolve(false);
|
|
237
|
-
|
|
238
|
-
Promise failedProbePromise = mock(Promise.class);
|
|
239
|
-
doThrow(new IllegalStateException("probe capture failure"))
|
|
240
|
-
.when(nativeOperations)
|
|
241
|
-
.captureExternalProbe(anyString(), anyString(), anyString(), anyString(), any(Map.class), anyString());
|
|
242
|
-
module.captureProbe(
|
|
243
|
-
"checkout.cart",
|
|
244
|
-
readableMap(Map.of()),
|
|
245
|
-
"2026-07-28T12:00:00.000Z",
|
|
246
|
-
failedProbePromise);
|
|
247
|
-
verify(failedProbePromise).resolve(false);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
@Test
|
|
251
|
-
public void canonicalEventsNormalizeIntegralReactNativeNumbersRecursively() {
|
|
252
|
-
ReadableMap event = readableMap(Map.of(
|
|
253
|
-
"event_type", "request_event",
|
|
254
|
-
"payload", Map.of(
|
|
255
|
-
"response_status", 503.0,
|
|
256
|
-
"duration_ms", 12.5,
|
|
257
|
-
"probe_data", Map.of(
|
|
258
|
-
"version", 1.0,
|
|
259
|
-
"values", List.of(2.0, 2.5)))));
|
|
260
|
-
Promise promise = mock(Promise.class);
|
|
261
|
-
when(nativeOperations.captureExternalEvent(any(Map.class))).thenReturn(true);
|
|
262
|
-
|
|
263
|
-
module.enqueueCanonicalEvent(event, promise);
|
|
264
|
-
|
|
265
|
-
@SuppressWarnings("unchecked")
|
|
266
|
-
ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
|
|
267
|
-
verify(nativeOperations).captureExternalEvent(captor.capture());
|
|
268
|
-
Map<String, Object> payload = (Map<String, Object>) captor.getValue().get("payload");
|
|
269
|
-
Map<String, Object> probeData = (Map<String, Object>) payload.get("probe_data");
|
|
270
|
-
assertEquals(503L, payload.get("response_status"));
|
|
271
|
-
assertEquals(12.5, payload.get("duration_ms"));
|
|
272
|
-
assertEquals(1L, probeData.get("version"));
|
|
273
|
-
assertEquals(List.of(2L, 2.5), probeData.get("values"));
|
|
274
|
-
verify(promise).resolve(true);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
@Test
|
|
278
|
-
public void contextTriggerAndFlushOperationsAlwaysResolve() throws Exception {
|
|
279
|
-
Promise contextPromise = mock(Promise.class);
|
|
280
|
-
Dynamic tenant = dynamic(ReadableType.String, "checkout");
|
|
281
|
-
module.setContext("tenant", tenant, contextPromise);
|
|
282
|
-
verify(nativeOperations).setContext("tenant", "checkout");
|
|
283
|
-
verify(tenant).recycle();
|
|
284
|
-
verify(contextPromise).resolve(isNull());
|
|
285
|
-
|
|
286
|
-
Promise contextValuePromise = mock(Promise.class);
|
|
287
|
-
module.setContextValue(
|
|
288
|
-
"cart",
|
|
289
|
-
readableMap(Map.of("value", 2)),
|
|
290
|
-
contextValuePromise);
|
|
291
|
-
verify(nativeOperations).setContext("cart", 2);
|
|
292
|
-
verify(contextValuePromise).resolve(isNull());
|
|
293
|
-
|
|
294
|
-
Promise triggerPromise = mock(Promise.class);
|
|
295
|
-
when(nativeOperations.activateProbeTriggerToken("trigger")).thenReturn(true);
|
|
296
|
-
module.activateProbeTriggerToken("trigger", triggerPromise);
|
|
297
|
-
verify(triggerPromise).resolve(true);
|
|
298
|
-
|
|
299
|
-
Promise flushPromise = mock(Promise.class);
|
|
300
|
-
module.flush(flushPromise);
|
|
301
|
-
verify(nativeOperations).flush();
|
|
302
|
-
verify(flushPromise).resolve(isNull());
|
|
303
|
-
|
|
304
|
-
doThrow(new IllegalStateException("context failure"))
|
|
305
|
-
.when(nativeOperations)
|
|
306
|
-
.setContext(eq("broken"), any());
|
|
307
|
-
Promise failedContext = mock(Promise.class);
|
|
308
|
-
Dynamic failedValue = dynamic(ReadableType.String, "value");
|
|
309
|
-
module.setContext("broken", failedValue, failedContext);
|
|
310
|
-
verify(failedValue).recycle();
|
|
311
|
-
verify(failedContext).resolve(isNull());
|
|
312
|
-
|
|
313
|
-
doThrow(new ReflectiveOperationException("flush failure"))
|
|
314
|
-
.when(nativeOperations)
|
|
315
|
-
.flush();
|
|
316
|
-
Promise failedFlush = mock(Promise.class);
|
|
317
|
-
module.flush(failedFlush);
|
|
318
|
-
verify(failedFlush).resolve(isNull());
|
|
319
|
-
|
|
320
|
-
doThrow(new IllegalStateException("trigger failure"))
|
|
321
|
-
.when(nativeOperations)
|
|
322
|
-
.activateProbeTriggerToken("broken");
|
|
323
|
-
Promise failedTrigger = mock(Promise.class);
|
|
324
|
-
module.activateProbeTriggerToken("broken", failedTrigger);
|
|
325
|
-
verify(failedTrigger).resolve(false);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
@Test
|
|
329
|
-
public void legacyDynamicContextPreservesEverySupportedValueShape() {
|
|
330
|
-
assertDynamicContext("null", dynamic(ReadableType.Null, null), null);
|
|
331
|
-
assertDynamicContext("boolean", dynamic(ReadableType.Boolean, true), true);
|
|
332
|
-
assertDynamicContext("number", dynamic(ReadableType.Number, 2.5), 2.5);
|
|
333
|
-
assertDynamicContext("string", dynamic(ReadableType.String, "checkout"), "checkout");
|
|
334
|
-
assertDynamicContext(
|
|
335
|
-
"map",
|
|
336
|
-
dynamic(ReadableType.Map, readableMap(Map.of("count", 2))),
|
|
337
|
-
Map.of("count", 2));
|
|
338
|
-
assertDynamicContext(
|
|
339
|
-
"array",
|
|
340
|
-
dynamic(ReadableType.Array, readableArray(List.of("cart", 2))),
|
|
341
|
-
List.of("cart", 2));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
@Test
|
|
345
|
-
public void legacyTranslationFailureIsSwallowedAndModuleNameIsStable() {
|
|
346
|
-
Promise promise = mock(Promise.class);
|
|
347
|
-
doThrow(new IllegalStateException("legacy failure"))
|
|
348
|
-
.when(nativeOperations)
|
|
349
|
-
.captureMessage(anyString(), any(DebugBundleLogLevel.class), any(Map.class));
|
|
350
|
-
|
|
351
|
-
module.enqueueEvent(readableMap(Map.of("event_type", "custom_event")), promise);
|
|
352
|
-
|
|
353
|
-
verify(promise).resolve(isNull());
|
|
354
|
-
verify(promise, never()).reject(anyString(), anyString());
|
|
355
|
-
org.junit.Assert.assertEquals(DebugBundleReactNativeModule.NAME, module.getName());
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
@Test
|
|
359
|
-
public void reactPackageRegistersOneModuleAndNoViewManagers() {
|
|
360
|
-
DebugBundleReactNativePackage reactPackage = new DebugBundleReactNativePackage();
|
|
361
|
-
|
|
362
|
-
List<NativeModule> modules = reactPackage.createNativeModules(reactContext);
|
|
363
|
-
List<ViewManager> viewManagers = reactPackage.createViewManagers(reactContext);
|
|
364
|
-
|
|
365
|
-
org.junit.Assert.assertEquals(1, modules.size());
|
|
366
|
-
org.junit.Assert.assertEquals(DebugBundleReactNativeModule.NAME, modules.get(0).getName());
|
|
367
|
-
assertTrue(viewManagers.isEmpty());
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
@Test
|
|
371
|
-
public void newArchitectureInteropCanParseEveryExportedMethod() throws Exception {
|
|
372
|
-
Class<?> interop = Class.forName(
|
|
373
|
-
"com.facebook.react.internal.turbomodule.core.TurboModuleInteropUtils");
|
|
374
|
-
java.lang.reflect.Method parser = interop.getDeclaredMethod(
|
|
375
|
-
"getMethodDescriptorsFromModule",
|
|
376
|
-
NativeModule.class);
|
|
377
|
-
parser.setAccessible(true);
|
|
378
|
-
|
|
379
|
-
List<?> descriptors = (List<?>) parser.invoke(null, module);
|
|
380
|
-
|
|
381
|
-
assertFalse(descriptors.isEmpty());
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
private void assertLegacyEventCalls(
|
|
385
|
-
Map<String, Object> event,
|
|
386
|
-
Runnable verification) {
|
|
387
|
-
Promise promise = mock(Promise.class);
|
|
388
|
-
module.enqueueEvent(readableMap(event), promise);
|
|
389
|
-
verification.run();
|
|
390
|
-
verify(promise).resolve(isNull());
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
private void assertDynamicContext(String key, Dynamic value, Object expected) {
|
|
394
|
-
Promise promise = mock(Promise.class);
|
|
395
|
-
module.setContext(key, value, promise);
|
|
396
|
-
verify(nativeOperations).setContext(key, expected);
|
|
397
|
-
verify(value).recycle();
|
|
398
|
-
verify(promise).resolve(isNull());
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private static Dynamic dynamic(ReadableType type, Object value) {
|
|
402
|
-
Dynamic dynamic = mock(Dynamic.class);
|
|
403
|
-
when(dynamic.getType()).thenReturn(type);
|
|
404
|
-
when(dynamic.isNull()).thenReturn(type == ReadableType.Null);
|
|
405
|
-
switch (type) {
|
|
406
|
-
case Boolean:
|
|
407
|
-
when(dynamic.asBoolean()).thenReturn((Boolean) value);
|
|
408
|
-
break;
|
|
409
|
-
case Number:
|
|
410
|
-
when(dynamic.asDouble()).thenReturn((Double) value);
|
|
411
|
-
break;
|
|
412
|
-
case String:
|
|
413
|
-
when(dynamic.asString()).thenReturn((String) value);
|
|
414
|
-
break;
|
|
415
|
-
case Map:
|
|
416
|
-
when(dynamic.asMap()).thenReturn((ReadableMap) value);
|
|
417
|
-
break;
|
|
418
|
-
case Array:
|
|
419
|
-
when(dynamic.asArray()).thenReturn((ReadableArray) value);
|
|
420
|
-
break;
|
|
421
|
-
default:
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
return dynamic;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
428
|
-
private static ReadableArray readableArray(List<?> values) {
|
|
429
|
-
ReadableArray array = mock(ReadableArray.class);
|
|
430
|
-
when(array.toArrayList()).thenReturn((java.util.ArrayList) new java.util.ArrayList<>((List) values));
|
|
431
|
-
return array;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
435
|
-
private static ReadableMap readableMap(Map<String, ?> values) {
|
|
436
|
-
Map<String, Object> copy = new HashMap<>((Map) values);
|
|
437
|
-
ReadableMap readableMap = mock(ReadableMap.class);
|
|
438
|
-
when(readableMap.hasKey(anyString()))
|
|
439
|
-
.thenAnswer(invocation -> copy.containsKey(invocation.getArgument(0)));
|
|
440
|
-
when(readableMap.isNull(anyString()))
|
|
441
|
-
.thenAnswer(invocation -> copy.get(invocation.getArgument(0)) == null);
|
|
442
|
-
when(readableMap.getString(anyString()))
|
|
443
|
-
.thenAnswer(invocation -> (String) copy.get(invocation.getArgument(0)));
|
|
444
|
-
when(readableMap.getBoolean(anyString()))
|
|
445
|
-
.thenAnswer(invocation -> (Boolean) copy.get(invocation.getArgument(0)));
|
|
446
|
-
when(readableMap.getInt(anyString()))
|
|
447
|
-
.thenAnswer(invocation -> ((Number) copy.get(invocation.getArgument(0))).intValue());
|
|
448
|
-
when(readableMap.getDouble(anyString()))
|
|
449
|
-
.thenAnswer(invocation -> ((Number) copy.get(invocation.getArgument(0))).doubleValue());
|
|
450
|
-
when(readableMap.getMap(anyString()))
|
|
451
|
-
.thenAnswer(invocation -> {
|
|
452
|
-
Object value = copy.get(invocation.getArgument(0));
|
|
453
|
-
if (value instanceof ReadableMap) {
|
|
454
|
-
return value;
|
|
455
|
-
}
|
|
456
|
-
if (value instanceof Map) {
|
|
457
|
-
return readableMap((Map<String, ?>) value);
|
|
458
|
-
}
|
|
459
|
-
return null;
|
|
460
|
-
});
|
|
461
|
-
when(readableMap.getArray(anyString()))
|
|
462
|
-
.thenAnswer(invocation -> {
|
|
463
|
-
Object value = copy.get(invocation.getArgument(0));
|
|
464
|
-
if (!(value instanceof List)) {
|
|
465
|
-
return null;
|
|
466
|
-
}
|
|
467
|
-
ReadableArray array = mock(ReadableArray.class);
|
|
468
|
-
when(array.toArrayList()).thenReturn((java.util.ArrayList) new java.util.ArrayList<>((List) value));
|
|
469
|
-
return array;
|
|
470
|
-
});
|
|
471
|
-
when(readableMap.toHashMap()).thenReturn(new HashMap<>(copy));
|
|
472
|
-
return readableMap;
|
|
473
|
-
}
|
|
474
|
-
}
|