@debugbundle/sdk-react-native 0.1.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/DebugBundleReactNative.podspec +15 -0
- package/README.md +124 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +281 -0
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativePackage.java +20 -0
- package/app.plugin.js +52 -0
- package/dist/NativeDebugBundleReactNative.d.ts +13 -0
- package/dist/NativeDebugBundleReactNative.d.ts.map +1 -0
- package/dist/NativeDebugBundleReactNative.js +2 -0
- package/dist/app-state.d.ts +9 -0
- package/dist/app-state.d.ts.map +1 -0
- package/dist/app-state.js +10 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +328 -0
- package/dist/console.d.ts +3 -0
- package/dist/console.d.ts.map +1 -0
- package/dist/console.js +35 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/native.d.ts +7 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +59 -0
- package/dist/navigation.d.ts +13 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +26 -0
- package/dist/network.d.ts +14 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +179 -0
- package/dist/probes.d.ts +4 -0
- package/dist/probes.d.ts.map +1 -0
- package/dist/probes.js +8 -0
- package/dist/react.d.ts +21 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +36 -0
- package/dist/redaction.d.ts +12 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +132 -0
- package/dist/testing.d.ts +9 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +36 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/ios/DebugBundleReactNative.swift +125 -0
- package/ios/DebugBundleReactNativeBridge.m +28 -0
- package/package.json +99 -0
- package/src/NativeDebugBundleReactNative.ts +14 -0
- package/src/app-state.ts +17 -0
- package/src/client.ts +385 -0
- package/src/console.ts +38 -0
- package/src/errors.ts +64 -0
- package/src/index.ts +82 -0
- package/src/native.ts +79 -0
- package/src/navigation.tsx +37 -0
- package/src/network.ts +240 -0
- package/src/probes.ts +11 -0
- package/src/react-native-shim.d.ts +14 -0
- package/src/react.tsx +60 -0
- package/src/redaction.ts +165 -0
- package/src/testing.ts +45 -0
- package/src/types.ts +173 -0
package/src/redaction.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const DEFAULT_REDACT_FIELDS = [
|
|
2
|
+
"password",
|
|
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"
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_HEADER_ALLOWLIST = [
|
|
27
|
+
"user-agent",
|
|
28
|
+
"content-type",
|
|
29
|
+
"accept",
|
|
30
|
+
"x-request-id",
|
|
31
|
+
"x-correlation-id",
|
|
32
|
+
"x-debugbundle-trace-id",
|
|
33
|
+
"traceparent"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export interface SanitizeOptions {
|
|
37
|
+
redactFields?: string[];
|
|
38
|
+
maxDepth?: number;
|
|
39
|
+
maxStringLength?: number;
|
|
40
|
+
maxArrayLength?: number;
|
|
41
|
+
maxObjectKeys?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function defaultRedactFields(): string[] {
|
|
45
|
+
return [...DEFAULT_REDACT_FIELDS];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function sanitizeValue(value: unknown, options: SanitizeOptions = {}): unknown {
|
|
49
|
+
const visited = new WeakSet<object>();
|
|
50
|
+
return sanitize(value, normalizeOptions(options), 0, visited, undefined);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function sanitizeHeaders(
|
|
54
|
+
headers: Record<string, string> | undefined,
|
|
55
|
+
allowlist: string[] = DEFAULT_HEADER_ALLOWLIST,
|
|
56
|
+
options: SanitizeOptions = {}
|
|
57
|
+
): Record<string, string> {
|
|
58
|
+
if (!headers) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
const allowed = new Set(allowlist.map((header) => header.toLowerCase()));
|
|
62
|
+
const sanitized: Record<string, string> = {};
|
|
63
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
64
|
+
const normalizedKey = key.toLowerCase();
|
|
65
|
+
if (allowed.has(normalizedKey)) {
|
|
66
|
+
sanitized[normalizedKey] = String(sanitizeValue(value, options));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return sanitized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeOptions(options: SanitizeOptions): Required<SanitizeOptions> {
|
|
73
|
+
return {
|
|
74
|
+
redactFields: options.redactFields ?? DEFAULT_REDACT_FIELDS,
|
|
75
|
+
maxDepth: options.maxDepth ?? 8,
|
|
76
|
+
maxStringLength: options.maxStringLength ?? 2048,
|
|
77
|
+
maxArrayLength: options.maxArrayLength ?? 50,
|
|
78
|
+
maxObjectKeys: options.maxObjectKeys ?? 50
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sanitize(
|
|
83
|
+
value: unknown,
|
|
84
|
+
options: Required<SanitizeOptions>,
|
|
85
|
+
depth: number,
|
|
86
|
+
visited: WeakSet<object>,
|
|
87
|
+
key: string | undefined
|
|
88
|
+
): unknown {
|
|
89
|
+
if (key && isSensitiveKey(key, options.redactFields)) {
|
|
90
|
+
return "[Redacted]";
|
|
91
|
+
}
|
|
92
|
+
if (value === null || value === undefined) {
|
|
93
|
+
return value ?? null;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === "string") {
|
|
96
|
+
return truncate(value, options.maxStringLength);
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === "bigint") {
|
|
102
|
+
return value.toString();
|
|
103
|
+
}
|
|
104
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
105
|
+
return `[${typeof value}]`;
|
|
106
|
+
}
|
|
107
|
+
if (value instanceof Error) {
|
|
108
|
+
return sanitizeError(value, options, depth, visited);
|
|
109
|
+
}
|
|
110
|
+
if (depth >= options.maxDepth) {
|
|
111
|
+
return "[Truncated: depth]";
|
|
112
|
+
}
|
|
113
|
+
if (typeof value === "object") {
|
|
114
|
+
if (visited.has(value)) {
|
|
115
|
+
return "[Circular]";
|
|
116
|
+
}
|
|
117
|
+
visited.add(value);
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
return value
|
|
120
|
+
.slice(0, options.maxArrayLength)
|
|
121
|
+
.map((entry) => sanitize(entry, options, depth + 1, visited, undefined));
|
|
122
|
+
}
|
|
123
|
+
const output: Record<string, unknown> = {};
|
|
124
|
+
for (const [entryKey, entryValue] of Object.entries(value).slice(0, options.maxObjectKeys)) {
|
|
125
|
+
output[entryKey] = sanitize(entryValue, options, depth + 1, visited, entryKey);
|
|
126
|
+
}
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
return String(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sanitizeError(
|
|
133
|
+
error: Error,
|
|
134
|
+
options: Required<SanitizeOptions>,
|
|
135
|
+
depth: number,
|
|
136
|
+
visited: WeakSet<object>
|
|
137
|
+
): Record<string, unknown> {
|
|
138
|
+
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,
|
|
142
|
+
cause: "cause" in error ? sanitize(error.cause, options, depth + 1, visited, "cause") : null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function truncate(value: string, maxLength: number): string {
|
|
147
|
+
if (value.length <= maxLength) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
return `${value.slice(0, maxLength)}...[Truncated]`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isSensitiveKey(key: string, sensitiveFields: string[]): boolean {
|
|
154
|
+
const segments = splitKey(key);
|
|
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);
|
|
165
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { resetNativeModuleCacheForTesting, setNativeModuleForTesting } from "./native.js";
|
|
2
|
+
import type { DebugBundleEventEnvelope, NativeDebugBundleModule, NativeDebugBundleState, ResolvedDebugBundleConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface RecordingNativeModule extends NativeDebugBundleModule {
|
|
5
|
+
events: DebugBundleEventEnvelope[];
|
|
6
|
+
config: ResolvedDebugBundleConfig | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createRecordingNativeModule(initialState: NativeDebugBundleState = { status: "healthy", lastEventAt: null }): RecordingNativeModule {
|
|
10
|
+
const state = { ...initialState };
|
|
11
|
+
return {
|
|
12
|
+
events: [],
|
|
13
|
+
config: null,
|
|
14
|
+
initialize(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
return state;
|
|
17
|
+
},
|
|
18
|
+
enqueueEvent(event) {
|
|
19
|
+
this.events.push(event);
|
|
20
|
+
state.lastEventAt = Date.now();
|
|
21
|
+
},
|
|
22
|
+
flush() {
|
|
23
|
+
state.status = "healthy";
|
|
24
|
+
},
|
|
25
|
+
getStatus() {
|
|
26
|
+
return state;
|
|
27
|
+
},
|
|
28
|
+
setContext() {
|
|
29
|
+
return undefined;
|
|
30
|
+
},
|
|
31
|
+
activateProbeTriggerToken() {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function installRecordingNativeModule(module: RecordingNativeModule = createRecordingNativeModule()): RecordingNativeModule {
|
|
38
|
+
setNativeModuleForTesting(module);
|
|
39
|
+
return module;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resetDebugBundleNativeModule(): void {
|
|
43
|
+
setNativeModuleForTesting(undefined);
|
|
44
|
+
resetNativeModuleCacheForTesting();
|
|
45
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export type DebugBundleStatus = "healthy" | "degraded" | "disconnected";
|
|
2
|
+
|
|
3
|
+
export type DebugBundleLogLevel = "debug" | "info" | "warning" | "error" | "critical";
|
|
4
|
+
|
|
5
|
+
export interface DebugBundleConfig {
|
|
6
|
+
projectToken?: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
environment?: string;
|
|
9
|
+
service?: string;
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
releaseChannel?: string;
|
|
12
|
+
appVersion?: string;
|
|
13
|
+
buildNumber?: string;
|
|
14
|
+
batchSize?: number;
|
|
15
|
+
flushInterval?: number;
|
|
16
|
+
sampleRate?: number;
|
|
17
|
+
sessionSampleRate?: number;
|
|
18
|
+
maxEventsPerSession?: number;
|
|
19
|
+
maxBreadcrumbs?: number;
|
|
20
|
+
captureErrors?: boolean;
|
|
21
|
+
captureUnhandledRejections?: boolean;
|
|
22
|
+
captureScreens?: boolean;
|
|
23
|
+
captureActions?: boolean;
|
|
24
|
+
captureNetwork?: boolean;
|
|
25
|
+
captureConsole?: boolean;
|
|
26
|
+
captureLogs?: boolean;
|
|
27
|
+
logLevel?: DebugBundleLogLevel;
|
|
28
|
+
tracePropagationTargets?: string[];
|
|
29
|
+
networkFilter?: DebugBundleNetworkFilter;
|
|
30
|
+
offlineQueueMaxEvents?: number;
|
|
31
|
+
offlineQueueMaxBytes?: number;
|
|
32
|
+
offlineQueueTtl?: number;
|
|
33
|
+
requestTimeout?: number;
|
|
34
|
+
maxProbeLabels?: number;
|
|
35
|
+
maxProbeEntriesPerLabel?: number;
|
|
36
|
+
probeFlushOnError?: boolean;
|
|
37
|
+
redactFields?: string[];
|
|
38
|
+
headerAllowlist?: string[];
|
|
39
|
+
sdkVersion?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ResolvedDebugBundleConfig {
|
|
43
|
+
projectToken: string;
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
environment: string;
|
|
46
|
+
service: string;
|
|
47
|
+
endpoint: string;
|
|
48
|
+
releaseChannel: string;
|
|
49
|
+
appVersion: string | null;
|
|
50
|
+
buildNumber: string | null;
|
|
51
|
+
batchSize: number;
|
|
52
|
+
flushInterval: number;
|
|
53
|
+
sampleRate: number;
|
|
54
|
+
sessionSampleRate: number;
|
|
55
|
+
maxEventsPerSession: number;
|
|
56
|
+
maxBreadcrumbs: number;
|
|
57
|
+
captureErrors: boolean;
|
|
58
|
+
captureUnhandledRejections: boolean;
|
|
59
|
+
captureScreens: boolean;
|
|
60
|
+
captureActions: boolean;
|
|
61
|
+
captureNetwork: boolean;
|
|
62
|
+
captureConsole: boolean;
|
|
63
|
+
captureLogs: boolean;
|
|
64
|
+
logLevel: DebugBundleLogLevel;
|
|
65
|
+
tracePropagationTargets: string[];
|
|
66
|
+
networkFilter: DebugBundleNetworkFilter;
|
|
67
|
+
offlineQueueMaxEvents: number;
|
|
68
|
+
offlineQueueMaxBytes: number;
|
|
69
|
+
offlineQueueTtl: number;
|
|
70
|
+
requestTimeout: number;
|
|
71
|
+
maxProbeLabels: number;
|
|
72
|
+
maxProbeEntriesPerLabel: number;
|
|
73
|
+
probeFlushOnError: boolean;
|
|
74
|
+
redactFields: string[];
|
|
75
|
+
headerAllowlist: string[];
|
|
76
|
+
sdkVersion: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DebugBundleNetworkFilter {
|
|
80
|
+
urlAllowlist?: string[];
|
|
81
|
+
urlDenylist?: string[];
|
|
82
|
+
statusCodes?: number[];
|
|
83
|
+
minResponseTime?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DebugBundleRequestInfo {
|
|
87
|
+
method: string;
|
|
88
|
+
url: string;
|
|
89
|
+
headers?: Record<string, string>;
|
|
90
|
+
routeTemplate?: string;
|
|
91
|
+
traceId?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DebugBundleResponseInfo {
|
|
95
|
+
statusCode: number;
|
|
96
|
+
durationMillis?: number;
|
|
97
|
+
headers?: Record<string, string>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface DebugBundleCaptureContext {
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface DebugBundleProbeOptions {
|
|
105
|
+
heavy?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface DebugBundleEventEnvelope {
|
|
109
|
+
schema_version: "1";
|
|
110
|
+
event_id: string;
|
|
111
|
+
event_type:
|
|
112
|
+
| "frontend_exception"
|
|
113
|
+
| "frontend_breadcrumb"
|
|
114
|
+
| "log_event"
|
|
115
|
+
| "request_event"
|
|
116
|
+
| "error_suppressed"
|
|
117
|
+
| "probe_event";
|
|
118
|
+
sdk_name: "@debugbundle/sdk-react-native";
|
|
119
|
+
sdk_version: string;
|
|
120
|
+
service: {
|
|
121
|
+
name: string;
|
|
122
|
+
environment: string;
|
|
123
|
+
runtime: "react-native";
|
|
124
|
+
framework: "react-native";
|
|
125
|
+
};
|
|
126
|
+
occurred_at: string;
|
|
127
|
+
correlation: {
|
|
128
|
+
trace_id: string | null;
|
|
129
|
+
request_id?: string | null;
|
|
130
|
+
session_id?: string | null;
|
|
131
|
+
user_id_hash?: string | null;
|
|
132
|
+
} | null;
|
|
133
|
+
payload: Record<string, unknown>;
|
|
134
|
+
device: Record<string, unknown> | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface DebugBundleClient {
|
|
138
|
+
readonly status: DebugBundleStatus;
|
|
139
|
+
readonly lastEventAt: number | null;
|
|
140
|
+
init(config: DebugBundleConfig): void;
|
|
141
|
+
captureException(error: unknown, context?: DebugBundleCaptureContext): void;
|
|
142
|
+
captureError(error: unknown, context?: DebugBundleCaptureContext): void;
|
|
143
|
+
captureLog(message: string, level?: DebugBundleLogLevel, context?: DebugBundleCaptureContext): void;
|
|
144
|
+
captureRequest(
|
|
145
|
+
request: DebugBundleRequestInfo,
|
|
146
|
+
response: DebugBundleResponseInfo,
|
|
147
|
+
context?: DebugBundleCaptureContext
|
|
148
|
+
): void;
|
|
149
|
+
captureMessage(message: string, level?: DebugBundleLogLevel, context?: DebugBundleCaptureContext): void;
|
|
150
|
+
setContext(key: string, value: unknown): void;
|
|
151
|
+
probe(label: string, data: unknown | (() => unknown), options?: DebugBundleProbeOptions): void;
|
|
152
|
+
flush(): Promise<void>;
|
|
153
|
+
activateProbeTriggerToken(token: string): Promise<boolean>;
|
|
154
|
+
recordBreadcrumb(type: string, data?: Record<string, unknown>): void;
|
|
155
|
+
recordScreen(screenName: string, previousScreen?: string | null, source?: string): void;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface NativeDebugBundleModule {
|
|
159
|
+
initialize(config: ResolvedDebugBundleConfig): Promise<NativeDebugBundleState> | NativeDebugBundleState;
|
|
160
|
+
enqueueEvent(event: DebugBundleEventEnvelope): Promise<void> | void;
|
|
161
|
+
flush(): Promise<void> | void;
|
|
162
|
+
getStatus(): Promise<NativeDebugBundleState> | NativeDebugBundleState;
|
|
163
|
+
setContext?(key: string, value: unknown): Promise<void> | void;
|
|
164
|
+
activateProbeTriggerToken?(token: string): Promise<boolean> | boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface NativeDebugBundleState {
|
|
168
|
+
status: DebugBundleStatus;
|
|
169
|
+
lastEventAt?: number | null;
|
|
170
|
+
device?: Record<string, unknown> | null;
|
|
171
|
+
nativeModuleAvailable?: boolean;
|
|
172
|
+
degradedReason?: string | null;
|
|
173
|
+
}
|