@debugbundle/sdk-react-native 3.0.0 → 3.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/CHANGELOG.md +23 -0
- package/DebugBundleReactNative.podspec +2 -2
- package/MIGRATION-3.1.md +116 -0
- package/README.md +38 -2
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +34 -3
- package/app.plugin.js +1 -1
- package/dist/NativeDebugBundleReactNative.d.ts +1 -0
- package/dist/NativeDebugBundleReactNative.d.ts.map +1 -1
- package/dist/before-send.d.ts.map +1 -1
- package/dist/before-send.js +9 -131
- package/dist/canonical-schema.d.ts +15 -0
- package/dist/canonical-schema.d.ts.map +1 -0
- package/dist/canonical-schema.js +139 -0
- package/dist/client-values.d.ts +17 -0
- package/dist/client-values.d.ts.map +1 -0
- package/dist/client-values.js +185 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +102 -205
- package/dist/diagnostic-budget.d.ts +16 -0
- package/dist/diagnostic-budget.d.ts.map +1 -0
- package/dist/diagnostic-budget.js +120 -0
- package/dist/event-schema-data.d.ts +2 -0
- package/dist/event-schema-data.d.ts.map +1 -0
- package/dist/event-schema-data.js +49 -0
- package/dist/evidence-capabilities.d.ts +5 -0
- package/dist/evidence-capabilities.d.ts.map +1 -0
- package/dist/evidence-capabilities.js +21 -0
- package/dist/evidence-controller.d.ts +24 -0
- package/dist/evidence-controller.d.ts.map +1 -0
- package/dist/evidence-controller.js +113 -0
- package/dist/evidence-types.d.ts +33 -0
- package/dist/evidence-types.d.ts.map +1 -0
- package/dist/evidence-types.js +1 -0
- package/dist/evidence-wire.d.ts +7 -0
- package/dist/evidence-wire.d.ts.map +1 -0
- package/dist/evidence-wire.js +132 -0
- package/dist/exception-evidence.d.ts +25 -0
- package/dist/exception-evidence.d.ts.map +1 -0
- package/dist/exception-evidence.js +175 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/owned-json.d.ts +11 -0
- package/dist/owned-json.d.ts.map +1 -0
- package/dist/owned-json.js +85 -0
- package/dist/privacy-keys.d.ts +2 -0
- package/dist/privacy-keys.d.ts.map +1 -1
- package/dist/privacy-keys.js +10 -2
- package/dist/privacy-telemetry.js +7 -7
- package/dist/privacy-text.d.ts +2 -2
- package/dist/privacy-text.d.ts.map +1 -1
- package/dist/privacy-text.js +35 -33
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +9 -7
- package/dist/safe-error-fields.d.ts +4 -0
- package/dist/safe-error-fields.d.ts.map +1 -0
- package/dist/safe-error-fields.js +54 -0
- package/dist/types.d.ts +21 -6
- package/dist/types.d.ts.map +1 -1
- package/ios/DebugBundleReactNative.swift +58 -4
- package/ios/DebugBundleReactNativeBridge.mm +4 -0
- package/package.json +3 -2
- package/src/NativeDebugBundleReactNative.ts +1 -0
- package/src/before-send.ts +10 -174
- package/src/canonical-schema.ts +108 -0
- package/src/client-values.ts +196 -0
- package/src/client.ts +92 -213
- package/src/diagnostic-budget.ts +90 -0
- package/src/event-schema-data.ts +49 -0
- package/src/evidence-capabilities.ts +19 -0
- package/src/evidence-controller.ts +89 -0
- package/src/evidence-types.ts +14 -0
- package/src/evidence-wire.ts +100 -0
- package/src/exception-evidence.ts +112 -0
- package/src/index.ts +6 -0
- package/src/owned-json.ts +60 -0
- package/src/privacy-keys.ts +12 -2
- package/src/privacy-telemetry.ts +9 -9
- package/src/privacy-text.ts +32 -31
- package/src/redaction.ts +8 -7
- package/src/safe-error-fields.ts +46 -0
- package/src/types.ts +23 -5
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Read primitive native error fields without calling application accessors. */
|
|
2
|
+
const nativePrototypes = new Set([Error.prototype, TypeError.prototype, RangeError.prototype, ReferenceError.prototype,
|
|
3
|
+
SyntaxError.prototype, URIError.prototype, EvalError.prototype, ...(typeof AggregateError === "function" ? [AggregateError.prototype] : [])]);
|
|
4
|
+
const brandDescriptor = Object.getOwnPropertyDescriptor(Error, "isError");
|
|
5
|
+
export const nativeBrand = brandDescriptor && "value" in brandDescriptor && typeof brandDescriptor.value === "function"
|
|
6
|
+
? brandDescriptor.value : undefined;
|
|
7
|
+
let nativeStackGetter;
|
|
8
|
+
export function errorField(value, key) {
|
|
9
|
+
const own = Object.getOwnPropertyDescriptor(value, key);
|
|
10
|
+
if (own)
|
|
11
|
+
return "value" in own ? own.value : undefined;
|
|
12
|
+
let prototype = Object.getPrototypeOf(value);
|
|
13
|
+
for (let depth = 0; prototype && depth < 4; depth++) {
|
|
14
|
+
if (!nativePrototypes.has(prototype))
|
|
15
|
+
return undefined;
|
|
16
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
|
17
|
+
if (descriptor)
|
|
18
|
+
return "value" in descriptor ? descriptor.value : undefined;
|
|
19
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
export function safeErrorStack(value) {
|
|
24
|
+
let prototype = Object.getPrototypeOf(value);
|
|
25
|
+
for (let depth = 0; prototype && depth < 4; depth++) {
|
|
26
|
+
if (!nativePrototypes.has(prototype) && prototype !== Object.prototype)
|
|
27
|
+
return undefined;
|
|
28
|
+
for (const key of ["name", "message"]) {
|
|
29
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
|
30
|
+
if (descriptor && (!("value" in descriptor) || typeof descriptor.value !== "string" || descriptor.value.length > 8192))
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
34
|
+
}
|
|
35
|
+
for (const key of ["name", "message"]) {
|
|
36
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
37
|
+
if (descriptor && (!("value" in descriptor) || typeof descriptor.value !== "string" || descriptor.value.length > 8192))
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const formatter = Object.getOwnPropertyDescriptor(Error, "prepareStackTrace");
|
|
41
|
+
const limit = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
|
|
42
|
+
if (formatter && (!("value" in formatter) || formatter.value !== undefined) ||
|
|
43
|
+
limit && (!("value" in limit) || typeof limit.value !== "number" || limit.value < 0 || limit.value > 64))
|
|
44
|
+
return undefined;
|
|
45
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, "stack");
|
|
46
|
+
if (descriptor && "value" in descriptor)
|
|
47
|
+
return descriptor.value;
|
|
48
|
+
if (descriptor?.get && nativeBrand?.(value)) {
|
|
49
|
+
nativeStackGetter ??= Object.getOwnPropertyDescriptor(new Error(), "stack")?.get;
|
|
50
|
+
if (descriptor.get === nativeStackGetter)
|
|
51
|
+
return descriptor.get.call(value);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface DebugBundleConfig {
|
|
|
17
17
|
maxEventsPerSession?: number;
|
|
18
18
|
maxBreadcrumbs?: number;
|
|
19
19
|
captureErrors?: boolean;
|
|
20
|
+
captureExceptionEvidence?: boolean;
|
|
21
|
+
captureRecoveryObservations?: boolean;
|
|
22
|
+
evidenceCapabilities?: EvidenceCapabilities;
|
|
20
23
|
captureUnhandledRejections?: boolean;
|
|
21
24
|
captureScreens?: boolean;
|
|
22
25
|
captureActions?: boolean;
|
|
@@ -54,6 +57,9 @@ export interface ResolvedDebugBundleConfig {
|
|
|
54
57
|
maxEventsPerSession: number;
|
|
55
58
|
maxBreadcrumbs: number;
|
|
56
59
|
captureErrors: boolean;
|
|
60
|
+
captureExceptionEvidence: boolean;
|
|
61
|
+
captureRecoveryObservations: boolean;
|
|
62
|
+
evidenceCapabilities: EvidenceCapabilities | null;
|
|
57
63
|
captureUnhandledRejections: boolean;
|
|
58
64
|
captureScreens: boolean;
|
|
59
65
|
captureActions: boolean;
|
|
@@ -94,26 +100,28 @@ export interface DebugBundleResponseInfo {
|
|
|
94
100
|
headers?: Record<string, string>;
|
|
95
101
|
}
|
|
96
102
|
export interface DebugBundleCaptureContext {
|
|
103
|
+
failureRef?: string;
|
|
104
|
+
evidenceScope?: EvidenceScope;
|
|
97
105
|
[key: string]: unknown;
|
|
98
106
|
}
|
|
99
107
|
export interface DebugBundleProbeOptions {
|
|
100
108
|
heavy?: boolean;
|
|
101
109
|
}
|
|
102
110
|
export interface DebugBundleEventEnvelope {
|
|
103
|
-
schema_version:
|
|
111
|
+
schema_version: string;
|
|
104
112
|
event_id: string;
|
|
105
|
-
event_type: "frontend_exception" | "frontend_breadcrumb" | "log_event" | "request_event" | "error_suppressed" | "probe_event";
|
|
106
|
-
sdk_name:
|
|
113
|
+
event_type: "frontend_exception" | "backend_exception" | "diagnostic_context" | "deploy_metadata" | "frontend_breadcrumb" | "log_event" | "request_event" | "error_suppressed" | "probe_event";
|
|
114
|
+
sdk_name: string;
|
|
107
115
|
sdk_version: string;
|
|
108
116
|
service: {
|
|
109
117
|
name: string;
|
|
110
118
|
environment: string;
|
|
111
|
-
runtime
|
|
112
|
-
framework
|
|
119
|
+
runtime?: string | null;
|
|
120
|
+
framework?: string | null;
|
|
113
121
|
};
|
|
114
122
|
occurred_at: string;
|
|
115
123
|
correlation?: {
|
|
116
|
-
trace_id
|
|
124
|
+
trace_id?: string | null;
|
|
117
125
|
request_id?: string | null;
|
|
118
126
|
session_id?: string | null;
|
|
119
127
|
user_id_hash?: string | null;
|
|
@@ -124,6 +132,10 @@ export interface DebugBundleEventEnvelope {
|
|
|
124
132
|
export interface DebugBundleClient {
|
|
125
133
|
readonly status: DebugBundleStatus;
|
|
126
134
|
readonly lastEventAt: number | null;
|
|
135
|
+
readonly evidenceCapabilities?: EvidenceCapabilities;
|
|
136
|
+
beginOperation?(): OperationScope;
|
|
137
|
+
withOperationContext?<T>(operation: (scope: OperationScope) => T): T;
|
|
138
|
+
captureRecovery?(observation: RecoveryObservation): void;
|
|
127
139
|
init(config: DebugBundleConfig): void;
|
|
128
140
|
captureException(error: unknown, context?: DebugBundleCaptureContext): void;
|
|
129
141
|
captureError(error: unknown, context?: DebugBundleCaptureContext): void;
|
|
@@ -140,6 +152,7 @@ export interface DebugBundleClient {
|
|
|
140
152
|
export interface NativeDebugBundleModule {
|
|
141
153
|
initialize(config: NativeDebugBundleConfig): Promise<NativeDebugBundleState> | NativeDebugBundleState;
|
|
142
154
|
enqueueCanonicalEvent?(event: DebugBundleEventEnvelope): Promise<boolean | void> | boolean | void;
|
|
155
|
+
enqueueCanonicalEventJson?(event: string): Promise<boolean | void> | boolean | void;
|
|
143
156
|
/** Compatibility fallback implemented by native binaries released before the canonical bridge. */
|
|
144
157
|
enqueueEvent(event: DebugBundleEventEnvelope): Promise<void> | void;
|
|
145
158
|
captureProbe?(label: string, data: unknown, occurredAt: string): Promise<boolean> | boolean;
|
|
@@ -157,7 +170,9 @@ export interface NativeDebugBundleState {
|
|
|
157
170
|
status: DebugBundleStatus;
|
|
158
171
|
lastEventAt?: number | null;
|
|
159
172
|
device?: Record<string, unknown> | null;
|
|
173
|
+
evidenceCapabilities?: unknown;
|
|
160
174
|
nativeModuleAvailable?: boolean;
|
|
161
175
|
degradedReason?: string | null;
|
|
162
176
|
}
|
|
177
|
+
import type { EvidenceCapabilities, EvidenceScope, OperationScope, RecoveryObservation } from "./evidence-types.js";
|
|
163
178
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import DebugBundle
|
|
|
4
4
|
|
|
5
5
|
@objc(DebugBundleReactNative)
|
|
6
6
|
final class DebugBundleReactNative: NSObject {
|
|
7
|
-
private var currentSDKVersion = "3.
|
|
7
|
+
private var currentSDKVersion = "3.1.0"
|
|
8
8
|
private var currentService = "react-native-app"
|
|
9
9
|
private var currentEnvironment = "production"
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ final class DebugBundleReactNative: NSObject {
|
|
|
14
14
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
15
15
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
16
16
|
) {
|
|
17
|
-
currentSDKVersion = config["sdkVersion"] as? String ?? "3.
|
|
17
|
+
currentSDKVersion = config["sdkVersion"] as? String ?? "3.1.0"
|
|
18
18
|
currentService = config["service"] as? String ?? "react-native-app"
|
|
19
19
|
currentEnvironment = config["environment"] as? String ?? "production"
|
|
20
20
|
let sdkConfig = DebugBundleConfig(
|
|
@@ -48,7 +48,11 @@ final class DebugBundleReactNative: NSObject {
|
|
|
48
48
|
redactFields: stringSet(config["redactFields"], fallback: DebugBundleConfig.defaultRedactFields),
|
|
49
49
|
headerAllowlist: stringSet(config["headerAllowlist"], fallback: DebugBundleConfig.defaultHeaderAllowlist),
|
|
50
50
|
sdkVersion: currentSDKVersion
|
|
51
|
-
)
|
|
51
|
+
).withEvidenceOptions(DebugBundleEvidenceOptions(
|
|
52
|
+
captureExceptionEvidence: config["captureExceptionEvidence"] as? Bool ?? true,
|
|
53
|
+
captureRecoveryObservations: config["captureRecoveryObservations"] as? Bool ?? false,
|
|
54
|
+
evidenceCapabilities: evidenceCapabilities(config["evidenceCapabilities"])
|
|
55
|
+
))
|
|
52
56
|
DebugBundle.initialize(sdkConfig, transport: DebugBundleHTTPTransport())
|
|
53
57
|
resolve(state())
|
|
54
58
|
}
|
|
@@ -62,6 +66,22 @@ final class DebugBundleReactNative: NSObject {
|
|
|
62
66
|
resolve(DebugBundle.captureExternalEvent(dictionary(event)))
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
@objc(enqueueCanonicalEventJson:resolve:reject:)
|
|
70
|
+
func enqueueCanonicalEventJson(
|
|
71
|
+
_ event: String,
|
|
72
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
73
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
74
|
+
) {
|
|
75
|
+
// TurboModule's NSDictionary conversion drops nested nulls before Swift sees
|
|
76
|
+
// them. Decode only bounded JSON bytes, then use the same native admission path.
|
|
77
|
+
guard let data = boundedEventJSON(event),
|
|
78
|
+
let object = try? JSONSerialization.jsonObject(with: data) as? NSDictionary else {
|
|
79
|
+
resolve(false)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
resolve(DebugBundle.captureExternalEvent(dictionary(object)))
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
@objc(enqueueEvent:resolve:reject:)
|
|
66
86
|
func enqueueEvent(
|
|
67
87
|
_ event: NSDictionary,
|
|
@@ -177,11 +197,45 @@ final class DebugBundleReactNative: NSObject {
|
|
|
177
197
|
[
|
|
178
198
|
"status": DebugBundle.status.rawValue,
|
|
179
199
|
"lastEventAt": DebugBundle.lastEventAt.map { $0.timeIntervalSince1970 * 1000 } ?? NSNull(),
|
|
180
|
-
"nativeModuleAvailable": true
|
|
200
|
+
"nativeModuleAvailable": true,
|
|
201
|
+
"evidenceCapabilities": ["version": DebugBundle.evidenceCapabilities.version, "features": DebugBundle.evidenceCapabilities.features]
|
|
181
202
|
]
|
|
182
203
|
}
|
|
183
204
|
}
|
|
184
205
|
|
|
206
|
+
private func evidenceCapabilities(_ value: Any?) -> DebugBundleEvidenceCapabilities? {
|
|
207
|
+
guard let value, !(value is NSNull) else { return nil }
|
|
208
|
+
guard let descriptor = value as? [String: Any], Set(descriptor.keys) == Set(["version", "features"]),
|
|
209
|
+
let features = descriptor["features"] as? [String], features.count <= 4,
|
|
210
|
+
features.allSatisfy({ $0.utf8.prefix(65).count <= 64 }),
|
|
211
|
+
let data = try? JSONSerialization.data(withJSONObject: descriptor), data.count <= 512,
|
|
212
|
+
let decoded = try? JSONDecoder().decode(DebugBundleEvidenceCapabilities.self, from: data) else {
|
|
213
|
+
return DebugBundleEvidenceCapabilities()
|
|
214
|
+
}
|
|
215
|
+
return decoded
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private func boundedEventJSON(_ value: String) -> Data? {
|
|
219
|
+
guard value.utf8.prefix(65_537).count <= 65_536 else { return nil }
|
|
220
|
+
var depth = 0, quoted = false, escaped = false
|
|
221
|
+
for byte in value.utf8 {
|
|
222
|
+
if quoted {
|
|
223
|
+
if escaped { escaped = false }
|
|
224
|
+
else if byte == 92 { escaped = true }
|
|
225
|
+
else if byte == 34 { quoted = false }
|
|
226
|
+
} else if byte == 34 { quoted = true }
|
|
227
|
+
else if byte == 123 || byte == 91 {
|
|
228
|
+
depth += 1
|
|
229
|
+
if depth > 17 { return nil }
|
|
230
|
+
} else if byte == 125 || byte == 93 {
|
|
231
|
+
depth -= 1
|
|
232
|
+
if depth < 0 { return nil }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
guard depth == 0, !quoted else { return nil }
|
|
236
|
+
return value.data(using: .utf8)
|
|
237
|
+
}
|
|
238
|
+
|
|
185
239
|
private func dictionary(_ value: NSDictionary) -> [String: Any?] {
|
|
186
240
|
var result: [String: Any?] = [:]
|
|
187
241
|
for (rawKey, rawValue) in value {
|
|
@@ -24,6 +24,10 @@ RCT_EXTERN_METHOD(enqueueCanonicalEvent:(NSDictionary *)event
|
|
|
24
24
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
25
25
|
reject:(RCTPromiseRejectBlock)reject)
|
|
26
26
|
|
|
27
|
+
RCT_EXTERN_METHOD(enqueueCanonicalEventJson:(NSString *)event
|
|
28
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
29
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
30
|
+
|
|
27
31
|
RCT_EXTERN_METHOD(flush:(RCTPromiseResolveBlock)resolve
|
|
28
32
|
reject:(RCTPromiseRejectBlock)reject)
|
|
29
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@debugbundle/sdk-react-native",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "React Native SDK for DebugBundle mobile event capture.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"DebugBundleReactNative.podspec",
|
|
50
50
|
"CHANGELOG.md",
|
|
51
51
|
"README.md",
|
|
52
|
-
"MIGRATION-3.0.md"
|
|
52
|
+
"MIGRATION-3.0.md",
|
|
53
|
+
"MIGRATION-3.1.md"
|
|
53
54
|
],
|
|
54
55
|
"exports": {
|
|
55
56
|
"./package.json": "./package.json",
|
|
@@ -5,6 +5,7 @@ import { TurboModuleRegistry } from "react-native";
|
|
|
5
5
|
export interface Spec extends TurboModule {
|
|
6
6
|
initialize(config: UnsafeObject): Promise<UnsafeObject>;
|
|
7
7
|
enqueueCanonicalEvent?(event: UnsafeObject): Promise<boolean>;
|
|
8
|
+
enqueueCanonicalEventJson?(event: string): Promise<boolean>;
|
|
8
9
|
enqueueEvent(event: UnsafeObject): Promise<void>;
|
|
9
10
|
captureProbe?(label: string, data: UnsafeObject, occurredAt: string): Promise<boolean>;
|
|
10
11
|
isProbeActive?(label: string): boolean;
|
package/src/before-send.ts
CHANGED
|
@@ -1,178 +1,14 @@
|
|
|
1
1
|
import type { DebugBundleBeforeSend, DebugBundleEventEnvelope } from "./types.js";
|
|
2
|
+
import { canonicalEvent } from "./evidence-wire.js";
|
|
3
|
+
import { EventValidationUnavailable, ownJSON } from "./owned-json.js";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"event_id",
|
|
6
|
-
"event_type",
|
|
7
|
-
"sdk_name",
|
|
8
|
-
"sdk_version",
|
|
9
|
-
"service",
|
|
10
|
-
"occurred_at",
|
|
11
|
-
"correlation",
|
|
12
|
-
"context",
|
|
13
|
-
"payload"
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
const REQUIRED_PAYLOAD_FIELDS: Record<DebugBundleEventEnvelope["event_type"], string[]> = {
|
|
17
|
-
frontend_exception: ["name", "message", "stack"],
|
|
18
|
-
frontend_breadcrumb: ["breadcrumb_type", "data"],
|
|
19
|
-
log_event: ["level", "message", "attributes"],
|
|
20
|
-
request_event: ["method", "path", "query", "headers", "response_status", "duration_ms"],
|
|
21
|
-
error_suppressed: ["fingerprint", "suppressed_count", "window_seconds", "first_seen", "last_seen"],
|
|
22
|
-
probe_event: ["label", "data", "activation_id", "probe_label_pattern"]
|
|
23
|
-
};
|
|
24
|
-
const ALLOWED_PAYLOAD_FIELDS: Record<DebugBundleEventEnvelope["event_type"], Set<string>> = {
|
|
25
|
-
frontend_exception: new Set([
|
|
26
|
-
"name", "message", "stack", "route", "browser", "breadcrumbs", "device",
|
|
27
|
-
"browser_event", "rejection_reason", "dom_context", "probe_data"
|
|
28
|
-
]),
|
|
29
|
-
frontend_breadcrumb: new Set(["breadcrumb_type", "route", "data", "device"]),
|
|
30
|
-
log_event: new Set(["level", "message", "attributes", "device"]),
|
|
31
|
-
request_event: new Set([
|
|
32
|
-
"method", "path", "query", "headers", "body", "response_status", "duration_ms",
|
|
33
|
-
"route_template", "response_headers", "response_body", "device"
|
|
34
|
-
]),
|
|
35
|
-
error_suppressed: new Set([
|
|
36
|
-
"fingerprint", "suppressed_count", "window_seconds", "first_seen", "last_seen", "device"
|
|
37
|
-
]),
|
|
38
|
-
probe_event: new Set(["label", "data", "activation_id", "probe_label_pattern", "device"])
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const EVENT_TYPES = new Set(Object.keys(REQUIRED_PAYLOAD_FIELDS));
|
|
42
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
43
|
-
|
|
44
|
-
export function applyBeforeSend(
|
|
45
|
-
event: DebugBundleEventEnvelope,
|
|
46
|
-
hook: DebugBundleBeforeSend | null
|
|
47
|
-
): DebugBundleEventEnvelope | null {
|
|
48
|
-
if (!hook) {
|
|
49
|
-
return event;
|
|
50
|
-
}
|
|
5
|
+
export function applyBeforeSend(event: DebugBundleEventEnvelope, hook: DebugBundleBeforeSend | null): DebugBundleEventEnvelope | null {
|
|
6
|
+
if (!hook) return event;
|
|
51
7
|
try {
|
|
52
|
-
const result = hook(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
if (result === null) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
return isValidEvent(result) ? result : event;
|
|
63
|
-
} catch {
|
|
64
|
-
return event;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function cloneEvent(event: DebugBundleEventEnvelope): DebugBundleEventEnvelope {
|
|
69
|
-
return JSON.parse(JSON.stringify(event)) as DebugBundleEventEnvelope;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isValidEvent(event: unknown): event is DebugBundleEventEnvelope {
|
|
73
|
-
if (!isRecord(event) || !Object.keys(event).every((key) => ROOT_KEYS.has(key))) {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
if (
|
|
77
|
-
event.schema_version !== "2026-03-01" ||
|
|
78
|
-
!UUID_PATTERN.test(stringValue(event.event_id)) ||
|
|
79
|
-
!EVENT_TYPES.has(stringValue(event.event_type)) ||
|
|
80
|
-
event.sdk_name !== "@debugbundle/sdk-react-native" ||
|
|
81
|
-
!nonEmptyString(event.sdk_version) ||
|
|
82
|
-
!isIsoTimestamp(event.occurred_at) ||
|
|
83
|
-
!isRecord(event.service) ||
|
|
84
|
-
!nonEmptyString(event.service.name) ||
|
|
85
|
-
!nonEmptyString(event.service.environment) ||
|
|
86
|
-
event.service.runtime !== "react-native" ||
|
|
87
|
-
event.service.framework !== "react-native" ||
|
|
88
|
-
!isRecord(event.payload)
|
|
89
|
-
) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
const eventType = event.event_type as DebugBundleEventEnvelope["event_type"];
|
|
93
|
-
return (
|
|
94
|
-
REQUIRED_PAYLOAD_FIELDS[eventType].every((field) =>
|
|
95
|
-
Object.prototype.hasOwnProperty.call(event.payload, field)
|
|
96
|
-
) &&
|
|
97
|
-
Object.keys(event.payload).every((field) => ALLOWED_PAYLOAD_FIELDS[eventType].has(field)) &&
|
|
98
|
-
hasValidPayloadShape(eventType, event.payload)
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function hasValidPayloadShape(
|
|
103
|
-
eventType: DebugBundleEventEnvelope["event_type"],
|
|
104
|
-
payload: Record<string, unknown>
|
|
105
|
-
): boolean {
|
|
106
|
-
switch (eventType) {
|
|
107
|
-
case "frontend_exception":
|
|
108
|
-
return (
|
|
109
|
-
hasNonEmptyStrings(payload, "name", "message", "stack") &&
|
|
110
|
-
(!("breadcrumbs" in payload) || Array.isArray(payload.breadcrumbs)) &&
|
|
111
|
-
optionalRecord(payload, "probe_data")
|
|
112
|
-
);
|
|
113
|
-
case "frontend_breadcrumb":
|
|
114
|
-
return hasNonEmptyStrings(payload, "breadcrumb_type") && isRecord(payload.data);
|
|
115
|
-
case "log_event":
|
|
116
|
-
return hasNonEmptyStrings(payload, "level", "message") && isRecord(payload.attributes);
|
|
117
|
-
case "request_event":
|
|
118
|
-
return (
|
|
119
|
-
hasNonEmptyStrings(payload, "method", "path") &&
|
|
120
|
-
isRecord(payload.query) &&
|
|
121
|
-
isRecord(payload.headers) &&
|
|
122
|
-
isNonNegativeNumber(payload.response_status) &&
|
|
123
|
-
isNonNegativeNumber(payload.duration_ms) &&
|
|
124
|
-
optionalRecord(payload, "response_headers")
|
|
125
|
-
);
|
|
126
|
-
case "error_suppressed":
|
|
127
|
-
return (
|
|
128
|
-
hasNonEmptyStrings(payload, "fingerprint") &&
|
|
129
|
-
isNonNegativeInteger(payload.suppressed_count) &&
|
|
130
|
-
isPositiveInteger(payload.window_seconds) &&
|
|
131
|
-
isIsoTimestamp(payload.first_seen) &&
|
|
132
|
-
isIsoTimestamp(payload.last_seen)
|
|
133
|
-
);
|
|
134
|
-
case "probe_event":
|
|
135
|
-
return (
|
|
136
|
-
hasNonEmptyStrings(payload, "label", "probe_label_pattern") &&
|
|
137
|
-
isRecord(payload.data) &&
|
|
138
|
-
(payload.activation_id === null ||
|
|
139
|
-
nonEmptyString(payload.activation_id) && UUID_PATTERN.test(payload.activation_id))
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function hasNonEmptyStrings(payload: Record<string, unknown>, ...fields: string[]): boolean {
|
|
145
|
-
return fields.every((field) => nonEmptyString(payload[field]));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function optionalRecord(payload: Record<string, unknown>, field: string): boolean {
|
|
149
|
-
return !(field in payload) || isRecord(payload[field]);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function isNonNegativeNumber(value: unknown): boolean {
|
|
153
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function isNonNegativeInteger(value: unknown): boolean {
|
|
157
|
-
return Number.isInteger(value) && (value as number) >= 0;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isPositiveInteger(value: unknown): boolean {
|
|
161
|
-
return Number.isInteger(value) && (value as number) > 0;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
165
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function nonEmptyString(value: unknown): value is string {
|
|
169
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function stringValue(value: unknown): string {
|
|
173
|
-
return typeof value === "string" ? value : "";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function isIsoTimestamp(value: unknown): boolean {
|
|
177
|
-
return nonEmptyString(value) && Number.isFinite(Date.parse(value));
|
|
8
|
+
const result = hook(JSON.parse(JSON.stringify(event)) as DebugBundleEventEnvelope);
|
|
9
|
+
if (result instanceof Promise) { void result.catch(() => undefined); return event; }
|
|
10
|
+
if (result === null) return null;
|
|
11
|
+
const owned = ownJSON(result);
|
|
12
|
+
return canonicalEvent(owned) ? owned as unknown as DebugBundleEventEnvelope : event;
|
|
13
|
+
} catch (error) { return error instanceof EventValidationUnavailable ? null : event; }
|
|
178
14
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { eventSchemaData } from "./event-schema-data.js";
|
|
2
|
+
import { EventValidationUnavailable, jsonObject, type OwnedJSON } from "./owned-json.js";
|
|
3
|
+
|
|
4
|
+
const KEYWORDS = new Set(["$schema", "$id", "$comment", "$ref", "type", "anyOf", "const", "enum", "properties",
|
|
5
|
+
"required", "additionalProperties", "items", "minimum", "maximum", "exclusiveMinimum", "minLength", "maxLength",
|
|
6
|
+
"minItems", "maxItems", "format", "pattern"]);
|
|
7
|
+
const TYPES = new Set(["null", "boolean", "number", "integer", "string", "array", "object"]);
|
|
8
|
+
const choices = (value: OwnedJSON | undefined): OwnedJSON[] => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
9
|
+
|
|
10
|
+
export function canonicalTimestamp(text: string): boolean {
|
|
11
|
+
if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$/.test(text)) return false;
|
|
12
|
+
const year = Number(text.slice(0, 4)), month = Number(text.slice(5, 7)), day = Number(text.slice(8, 10));
|
|
13
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
14
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
15
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1]! &&
|
|
16
|
+
Number(text.slice(11, 13)) < 24 && Number(text.slice(14, 16)) < 60 && Number(text.slice(17, 19)) < 60;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Small closed Draft-7 interpreter for the generated accepting contract. */
|
|
20
|
+
export class CanonicalSchema {
|
|
21
|
+
private refs = new Map<string, OwnedJSON>();
|
|
22
|
+
private patterns = new Map<string, RegExp>();
|
|
23
|
+
private root: OwnedJSON;
|
|
24
|
+
constructor(schema: unknown) { this.root = schema as OwnedJSON; this.compile(this.root); }
|
|
25
|
+
|
|
26
|
+
private resolve(ref: string): OwnedJSON {
|
|
27
|
+
const cached = this.refs.get(ref);
|
|
28
|
+
if (cached !== undefined) return cached;
|
|
29
|
+
if (!ref.startsWith("#/")) throw new EventValidationUnavailable();
|
|
30
|
+
let value: OwnedJSON | undefined = this.root;
|
|
31
|
+
for (const part of ref.slice(2).split("/")) {
|
|
32
|
+
const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
33
|
+
value = Array.isArray(value) ? value[Number(key)] : jsonObject(value)?.[key];
|
|
34
|
+
if (value === undefined) throw new EventValidationUnavailable();
|
|
35
|
+
}
|
|
36
|
+
this.refs.set(ref, value);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private compile(schema: OwnedJSON): void {
|
|
41
|
+
if (typeof schema === "boolean") return;
|
|
42
|
+
const node = jsonObject(schema);
|
|
43
|
+
if (!node || Object.keys(node).some(key => !KEYWORDS.has(key)) ||
|
|
44
|
+
choices(node.type).some(value => typeof value !== "string" || !TYPES.has(value)) ||
|
|
45
|
+
node.format !== undefined && !["uuid", "date-time"].includes(String(node.format))) throw new EventValidationUnavailable();
|
|
46
|
+
if (typeof node.pattern === "string") this.patterns.set(node.pattern, new RegExp(node.pattern));
|
|
47
|
+
if (typeof node.$ref === "string" && !this.refs.has(node.$ref)) this.compile(this.resolve(node.$ref));
|
|
48
|
+
for (const child of Object.values(jsonObject(node.properties) ?? {})) this.compile(child);
|
|
49
|
+
for (const child of choices(node.anyOf)) this.compile(child);
|
|
50
|
+
for (const key of ["items", "additionalProperties"]) if (node[key] !== undefined) this.compile(node[key]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
accepts(value: OwnedJSON): boolean {
|
|
54
|
+
let steps = 100_000;
|
|
55
|
+
const check = (schema: OwnedJSON, value: OwnedJSON, depth: number): boolean => {
|
|
56
|
+
if (--steps < 0 || depth > 64) throw new EventValidationUnavailable();
|
|
57
|
+
if (typeof schema === "boolean") return schema;
|
|
58
|
+
const node = jsonObject(schema);
|
|
59
|
+
if (!node) throw new EventValidationUnavailable();
|
|
60
|
+
if (typeof node.$ref === "string") return check(this.resolve(node.$ref), value, depth + 1);
|
|
61
|
+
if (Array.isArray(node.anyOf) && !node.anyOf.some(child => check(child, value, depth + 1))) return false;
|
|
62
|
+
if (node.type !== undefined && !choices(node.type).some(type => this.matches(value, type))) return false;
|
|
63
|
+
if (node.const !== undefined && node.const !== value) return false;
|
|
64
|
+
if (Array.isArray(node.enum) && !node.enum.includes(value)) return false;
|
|
65
|
+
const object = jsonObject(value);
|
|
66
|
+
if (object) {
|
|
67
|
+
if (choices(node.required).some(key => typeof key !== "string" || !Object.hasOwn(object, key))) return false;
|
|
68
|
+
const fields = jsonObject(node.properties) ?? {};
|
|
69
|
+
for (const [key, child] of Object.entries(object)) {
|
|
70
|
+
if (!check((Object.hasOwn(fields, key) ? fields[key] : undefined) ?? node.additionalProperties ?? true, child, depth + 1)) return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
if (typeof node.minItems === "number" && value.length < node.minItems ||
|
|
75
|
+
typeof node.maxItems === "number" && value.length > node.maxItems) return false;
|
|
76
|
+
if (node.items !== undefined && !value.every(child => check(node.items!, child, depth + 1))) return false;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
if (typeof node.minLength === "number" && value.length < node.minLength ||
|
|
80
|
+
typeof node.maxLength === "number" && value.length > node.maxLength) return false;
|
|
81
|
+
if (typeof node.pattern === "string" && !this.patterns.get(node.pattern)!.test(value)) return false;
|
|
82
|
+
if (node.format === "uuid" && !/^[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}$/i.test(value)) return false;
|
|
83
|
+
if (node.format === "date-time" && !canonicalTimestamp(value)) return false;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === "number") {
|
|
86
|
+
if (typeof node.minimum === "number" && value < node.minimum || typeof node.maximum === "number" && value > node.maximum ||
|
|
87
|
+
typeof node.exclusiveMinimum === "number" && value <= node.exclusiveMinimum) return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
return check(this.root, value, 0);
|
|
92
|
+
}
|
|
93
|
+
private matches(value: OwnedJSON, type: OwnedJSON): boolean {
|
|
94
|
+
if (type === "integer") return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
95
|
+
if (type === "number") return typeof value === "number" && Number.isFinite(value);
|
|
96
|
+
if (type === "null") return value === null;
|
|
97
|
+
if (type === "array") return Array.isArray(value);
|
|
98
|
+
if (type === "object") return jsonObject(value) !== null;
|
|
99
|
+
return typeof value === type;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let schema: CanonicalSchema | null = null;
|
|
104
|
+
try { schema = new CanonicalSchema(eventSchemaData); } catch { /* Unknown generated keywords fail closed. */ }
|
|
105
|
+
export function canonicalShape(value: OwnedJSON): boolean {
|
|
106
|
+
if (!schema) throw new EventValidationUnavailable();
|
|
107
|
+
return schema.accepts(value);
|
|
108
|
+
}
|