@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.
Files changed (67) hide show
  1. package/DebugBundleReactNative.podspec +15 -0
  2. package/README.md +124 -0
  3. package/android/build.gradle +37 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +281 -0
  6. package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativePackage.java +20 -0
  7. package/app.plugin.js +52 -0
  8. package/dist/NativeDebugBundleReactNative.d.ts +13 -0
  9. package/dist/NativeDebugBundleReactNative.d.ts.map +1 -0
  10. package/dist/NativeDebugBundleReactNative.js +2 -0
  11. package/dist/app-state.d.ts +9 -0
  12. package/dist/app-state.d.ts.map +1 -0
  13. package/dist/app-state.js +10 -0
  14. package/dist/client.d.ts +36 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +328 -0
  17. package/dist/console.d.ts +3 -0
  18. package/dist/console.d.ts.map +1 -0
  19. package/dist/console.js +35 -0
  20. package/dist/errors.d.ts +6 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +42 -0
  23. package/dist/index.d.ts +25 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +59 -0
  26. package/dist/native.d.ts +7 -0
  27. package/dist/native.d.ts.map +1 -0
  28. package/dist/native.js +59 -0
  29. package/dist/navigation.d.ts +13 -0
  30. package/dist/navigation.d.ts.map +1 -0
  31. package/dist/navigation.js +26 -0
  32. package/dist/network.d.ts +14 -0
  33. package/dist/network.d.ts.map +1 -0
  34. package/dist/network.js +179 -0
  35. package/dist/probes.d.ts +4 -0
  36. package/dist/probes.d.ts.map +1 -0
  37. package/dist/probes.js +8 -0
  38. package/dist/react.d.ts +21 -0
  39. package/dist/react.d.ts.map +1 -0
  40. package/dist/react.js +36 -0
  41. package/dist/redaction.d.ts +12 -0
  42. package/dist/redaction.d.ts.map +1 -0
  43. package/dist/redaction.js +132 -0
  44. package/dist/testing.d.ts +9 -0
  45. package/dist/testing.d.ts.map +1 -0
  46. package/dist/testing.js +36 -0
  47. package/dist/types.d.ts +152 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +1 -0
  50. package/ios/DebugBundleReactNative.swift +125 -0
  51. package/ios/DebugBundleReactNativeBridge.m +28 -0
  52. package/package.json +99 -0
  53. package/src/NativeDebugBundleReactNative.ts +14 -0
  54. package/src/app-state.ts +17 -0
  55. package/src/client.ts +385 -0
  56. package/src/console.ts +38 -0
  57. package/src/errors.ts +64 -0
  58. package/src/index.ts +82 -0
  59. package/src/native.ts +79 -0
  60. package/src/navigation.tsx +37 -0
  61. package/src/network.ts +240 -0
  62. package/src/probes.ts +11 -0
  63. package/src/react-native-shim.d.ts +14 -0
  64. package/src/react.tsx +60 -0
  65. package/src/redaction.ts +165 -0
  66. package/src/testing.ts +45 -0
  67. package/src/types.ts +173 -0
@@ -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
+ }