@multiplayer-app/session-recorder-react-native 1.2.27 → 1.2.29
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/lib/module/patch/fetch.js +96 -59
- package/lib/module/patch/fetch.js.map +1 -1
- package/package.json +2 -2
- package/src/patch/fetch.ts +114 -66
|
@@ -24,6 +24,33 @@ function _tryReadFetchBody({
|
|
|
24
24
|
}
|
|
25
25
|
return `[Fetch] Cannot read body of type ${Object.prototype.toString.call(body)}`;
|
|
26
26
|
}
|
|
27
|
+
async function _tryReadResponseBody(response) {
|
|
28
|
+
try {
|
|
29
|
+
// Clone the response to avoid consuming the original stream.
|
|
30
|
+
const clonedResponse = response.clone();
|
|
31
|
+
const contentType = response.headers.get('content-type') || '';
|
|
32
|
+
if (contentType.includes('application/json')) {
|
|
33
|
+
const json = await clonedResponse.json();
|
|
34
|
+
return JSON.stringify(json);
|
|
35
|
+
} else if (contentType.includes('text/')) {
|
|
36
|
+
return await clonedResponse.text();
|
|
37
|
+
} else {
|
|
38
|
+
// For other content types, try text first, fallback to arrayBuffer
|
|
39
|
+
try {
|
|
40
|
+
return await clonedResponse.text();
|
|
41
|
+
} catch {
|
|
42
|
+
try {
|
|
43
|
+
const arrayBuffer = await clonedResponse.arrayBuffer();
|
|
44
|
+
return `[Fetch] Binary data (${arrayBuffer.byteLength} bytes)`;
|
|
45
|
+
} catch {
|
|
46
|
+
return '[Fetch] Unable to read response body';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return `[Fetch] Error reading response body: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
27
54
|
function _headersToObject(headers) {
|
|
28
55
|
const result = {};
|
|
29
56
|
headers.forEach((value, key) => {
|
|
@@ -32,6 +59,31 @@ function _headersToObject(headers) {
|
|
|
32
59
|
return result;
|
|
33
60
|
}
|
|
34
61
|
|
|
62
|
+
// Convert HeadersInit to a plain object without needing to construct a Request
|
|
63
|
+
function _headersInitToObject(headersInit) {
|
|
64
|
+
if (!headersInit) return {};
|
|
65
|
+
|
|
66
|
+
// Headers instance
|
|
67
|
+
if (typeof Headers !== 'undefined' && headersInit instanceof Headers) {
|
|
68
|
+
return _headersToObject(headersInit);
|
|
69
|
+
}
|
|
70
|
+
const result = {};
|
|
71
|
+
|
|
72
|
+
// Array of tuples
|
|
73
|
+
if (Array.isArray(headersInit)) {
|
|
74
|
+
for (const [key, value] of headersInit) {
|
|
75
|
+
result[String(key).toLowerCase()] = String(value);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Record<string, string>
|
|
81
|
+
for (const [key, value] of Object.entries(headersInit)) {
|
|
82
|
+
result[String(key).toLowerCase()] = String(value);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
35
87
|
// Only patch fetch if available and safe to do so
|
|
36
88
|
if (typeof fetch !== 'undefined' && typeof global !== 'undefined') {
|
|
37
89
|
// Store original fetch
|
|
@@ -40,72 +92,52 @@ if (typeof fetch !== 'undefined' && typeof global !== 'undefined') {
|
|
|
40
92
|
// Override fetch with safer implementation
|
|
41
93
|
global.fetch = async function (input, init) {
|
|
42
94
|
const networkRequest = {};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
|
|
96
|
+
// Capture request data
|
|
97
|
+
const inputIsRequest = typeof Request !== 'undefined' && input instanceof Request;
|
|
98
|
+
const safeToConstructRequest = !inputIsRequest || !input.bodyUsed;
|
|
99
|
+
|
|
100
|
+
// Only construct a new Request when it's safe (i.e., body not already used)
|
|
101
|
+
let requestForMetadata = null;
|
|
102
|
+
if (safeToConstructRequest) {
|
|
103
|
+
try {
|
|
104
|
+
requestForMetadata = new Request(input, init);
|
|
105
|
+
} catch {
|
|
106
|
+
requestForMetadata = null;
|
|
52
107
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
108
|
+
}
|
|
109
|
+
if (configs.recordRequestHeaders) {
|
|
110
|
+
if (requestForMetadata) {
|
|
111
|
+
networkRequest.requestHeaders = _headersToObject(requestForMetadata.headers);
|
|
112
|
+
} else if (inputIsRequest) {
|
|
113
|
+
networkRequest.requestHeaders = _headersToObject(input.headers);
|
|
114
|
+
} else {
|
|
115
|
+
networkRequest.requestHeaders = _headersInitToObject(init?.headers);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (configs.shouldRecordBody) {
|
|
119
|
+
const candidateBody = requestForMetadata ? requestForMetadata.body : inputIsRequest ? init?.body : init?.body;
|
|
120
|
+
if (!isNullish(candidateBody)) {
|
|
121
|
+
const requestBody = _tryReadFetchBody({
|
|
122
|
+
body: candidateBody
|
|
123
|
+
});
|
|
124
|
+
if (requestBody?.length && (typeof Blob !== 'undefined' ? new Blob([requestBody]).size <= configs.maxCapturingHttpPayloadSize : requestBody.length <= configs.maxCapturingHttpPayloadSize)) {
|
|
125
|
+
networkRequest.requestBody = requestBody;
|
|
63
126
|
}
|
|
64
127
|
}
|
|
65
|
-
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
66
130
|
// Make the actual fetch request
|
|
67
131
|
const response = await originalFetch(input, init);
|
|
68
132
|
|
|
69
|
-
// Capture response data
|
|
133
|
+
// Capture response data
|
|
70
134
|
if (configs.recordResponseHeaders) {
|
|
71
|
-
|
|
72
|
-
networkRequest.responseHeaders = _headersToObject(response.headers);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
console.warn('[Fetch Patch] Failed to capture response headers:', error);
|
|
75
|
-
}
|
|
135
|
+
networkRequest.responseHeaders = _headersToObject(response.headers);
|
|
76
136
|
}
|
|
77
137
|
if (configs.shouldRecordBody) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Check if response body is available and not consumed
|
|
83
|
-
if (response.body && !response.bodyUsed) {
|
|
84
|
-
try {
|
|
85
|
-
// Try cloning first (might fail in React Native)
|
|
86
|
-
const clonedResponse = response.clone();
|
|
87
|
-
responseBody = await clonedResponse.text();
|
|
88
|
-
} catch (cloneError) {
|
|
89
|
-
// If cloning fails, try to read from original response
|
|
90
|
-
// This is risky but we'll catch the error
|
|
91
|
-
try {
|
|
92
|
-
responseBody = await response.text();
|
|
93
|
-
// If we get here, we consumed the body, so we need to recreate the response
|
|
94
|
-
// This is a limitation - we can't both capture and preserve the body
|
|
95
|
-
console.warn('[Fetch Patch] Response body consumed for capture - user code may not be able to read it');
|
|
96
|
-
} catch (readError) {
|
|
97
|
-
console.warn('[Fetch Patch] Failed to read response body:', readError);
|
|
98
|
-
responseBody = '[Fetch] Unable to read response body';
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} else if (response.bodyUsed) {
|
|
102
|
-
responseBody = '[Fetch] Response body already consumed';
|
|
103
|
-
}
|
|
104
|
-
if (responseBody?.length && responseBody.length <= configs.maxCapturingHttpPayloadSize) {
|
|
105
|
-
networkRequest.responseBody = responseBody;
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
console.warn('[Fetch Patch] Failed to capture response body:', error);
|
|
138
|
+
const responseBody = await _tryReadResponseBody(response);
|
|
139
|
+
if (responseBody?.length && (typeof Blob !== 'undefined' ? new Blob([responseBody]).size <= configs.maxCapturingHttpPayloadSize : responseBody.length <= configs.maxCapturingHttpPayloadSize)) {
|
|
140
|
+
networkRequest.responseBody = responseBody;
|
|
109
141
|
}
|
|
110
142
|
}
|
|
111
143
|
|
|
@@ -114,8 +146,13 @@ if (typeof fetch !== 'undefined' && typeof global !== 'undefined') {
|
|
|
114
146
|
response.networkRequest = networkRequest;
|
|
115
147
|
return response;
|
|
116
148
|
} catch (error) {
|
|
117
|
-
//
|
|
118
|
-
|
|
149
|
+
// Even if the fetch fails, we can still capture the request data
|
|
150
|
+
// Attach captured request data to the thrown error for downstream handling
|
|
151
|
+
// @ts-ignore
|
|
152
|
+
if (error && typeof error === 'object') {
|
|
153
|
+
// @ts-ignore
|
|
154
|
+
error.networkRequest = networkRequest;
|
|
155
|
+
}
|
|
119
156
|
throw error;
|
|
120
157
|
}
|
|
121
158
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["isFormData","isNullish","isObject","isString","formDataToQuery","configs","_tryReadFetchBody","body","JSON","stringify","Object","prototype","toString","call","
|
|
1
|
+
{"version":3,"names":["isFormData","isNullish","isObject","isString","formDataToQuery","configs","_tryReadFetchBody","body","JSON","stringify","Object","prototype","toString","call","_tryReadResponseBody","response","clonedResponse","clone","contentType","headers","get","includes","json","text","arrayBuffer","byteLength","error","Error","message","_headersToObject","result","forEach","value","key","_headersInitToObject","headersInit","Headers","Array","isArray","String","toLowerCase","entries","fetch","global","originalFetch","input","init","networkRequest","inputIsRequest","Request","safeToConstructRequest","bodyUsed","requestForMetadata","recordRequestHeaders","requestHeaders","shouldRecordBody","candidateBody","requestBody","length","Blob","size","maxCapturingHttpPayloadSize","recordResponseHeaders","responseHeaders","responseBody","setPrototypeOf","defineProperty","console","warn","info"],"sourceRoot":"../../../src","sources":["patch/fetch.ts"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplayer-app/session-recorder-react-native",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.29",
|
|
4
4
|
"description": "Multiplayer Fullstack Session Recorder for React Native",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Multiplayer Software, Inc.",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"typescript": "^5.9.2"
|
|
108
108
|
},
|
|
109
109
|
"dependencies": {
|
|
110
|
-
"@multiplayer-app/session-recorder-common": "1.2.
|
|
110
|
+
"@multiplayer-app/session-recorder-common": "1.2.29",
|
|
111
111
|
"@opentelemetry/core": "2.0.1",
|
|
112
112
|
"@opentelemetry/exporter-trace-otlp-http": "0.203.0",
|
|
113
113
|
"@opentelemetry/instrumentation": "0.203.0",
|
package/src/patch/fetch.ts
CHANGED
|
@@ -35,6 +35,35 @@ function _tryReadFetchBody({
|
|
|
35
35
|
return `[Fetch] Cannot read body of type ${Object.prototype.toString.call(body)}`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
async function _tryReadResponseBody(response: Response): Promise<string | null> {
|
|
39
|
+
try {
|
|
40
|
+
// Clone the response to avoid consuming the original stream.
|
|
41
|
+
const clonedResponse = response.clone()
|
|
42
|
+
|
|
43
|
+
const contentType = response.headers.get('content-type') || ''
|
|
44
|
+
if (contentType.includes('application/json')) {
|
|
45
|
+
const json = await clonedResponse.json()
|
|
46
|
+
return JSON.stringify(json)
|
|
47
|
+
} else if (contentType.includes('text/')) {
|
|
48
|
+
return await clonedResponse.text()
|
|
49
|
+
} else {
|
|
50
|
+
// For other content types, try text first, fallback to arrayBuffer
|
|
51
|
+
try {
|
|
52
|
+
return await clonedResponse.text()
|
|
53
|
+
} catch {
|
|
54
|
+
try {
|
|
55
|
+
const arrayBuffer = await clonedResponse.arrayBuffer()
|
|
56
|
+
return `[Fetch] Binary data (${arrayBuffer.byteLength} bytes)`
|
|
57
|
+
} catch {
|
|
58
|
+
return '[Fetch] Unable to read response body'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return `[Fetch] Error reading response body: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
38
67
|
function _headersToObject(headers: Headers): Record<string, string> {
|
|
39
68
|
const result: Record<string, string> = {}
|
|
40
69
|
headers.forEach((value, key) => {
|
|
@@ -43,6 +72,33 @@ function _headersToObject(headers: Headers): Record<string, string> {
|
|
|
43
72
|
return result
|
|
44
73
|
}
|
|
45
74
|
|
|
75
|
+
// Convert HeadersInit to a plain object without needing to construct a Request
|
|
76
|
+
function _headersInitToObject(headersInit?: any): Record<string, string> {
|
|
77
|
+
if (!headersInit) return {}
|
|
78
|
+
|
|
79
|
+
// Headers instance
|
|
80
|
+
if (typeof Headers !== 'undefined' && headersInit instanceof Headers) {
|
|
81
|
+
return _headersToObject(headersInit)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result: Record<string, string> = {}
|
|
85
|
+
|
|
86
|
+
// Array of tuples
|
|
87
|
+
if (Array.isArray(headersInit)) {
|
|
88
|
+
for (const [key, value] of headersInit) {
|
|
89
|
+
result[String(key).toLowerCase()] = String(value)
|
|
90
|
+
}
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Record<string, string>
|
|
95
|
+
for (const [key, value] of Object.entries(headersInit as Record<string, string>)) {
|
|
96
|
+
result[String(key).toLowerCase()] = String(value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
}
|
|
101
|
+
|
|
46
102
|
// Only patch fetch if available and safe to do so
|
|
47
103
|
if (typeof fetch !== 'undefined' && typeof global !== 'undefined') {
|
|
48
104
|
// Store original fetch
|
|
@@ -60,83 +116,70 @@ if (typeof fetch !== 'undefined' && typeof global !== 'undefined') {
|
|
|
60
116
|
responseBody?: string
|
|
61
117
|
} = {}
|
|
62
118
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
119
|
+
// Capture request data
|
|
120
|
+
const inputIsRequest = typeof Request !== 'undefined' && input instanceof Request
|
|
121
|
+
const safeToConstructRequest = !inputIsRequest || !(input as Request).bodyUsed
|
|
122
|
+
|
|
123
|
+
// Only construct a new Request when it's safe (i.e., body not already used)
|
|
124
|
+
let requestForMetadata: Request | null = null
|
|
125
|
+
if (safeToConstructRequest) {
|
|
126
|
+
try {
|
|
127
|
+
requestForMetadata = new Request(input as RequestInfo, init)
|
|
128
|
+
} catch {
|
|
129
|
+
requestForMetadata = null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
66
132
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
133
|
+
if (configs.recordRequestHeaders) {
|
|
134
|
+
if (requestForMetadata) {
|
|
135
|
+
networkRequest.requestHeaders = _headersToObject(requestForMetadata.headers)
|
|
136
|
+
} else if (inputIsRequest) {
|
|
137
|
+
networkRequest.requestHeaders = _headersToObject((input as Request).headers)
|
|
138
|
+
} else {
|
|
139
|
+
networkRequest.requestHeaders = _headersInitToObject(init?.headers)
|
|
73
140
|
}
|
|
141
|
+
}
|
|
74
142
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
143
|
+
if (configs.shouldRecordBody) {
|
|
144
|
+
const candidateBody: any | null | undefined = requestForMetadata
|
|
145
|
+
? (requestForMetadata as any).body as any
|
|
146
|
+
: (inputIsRequest ? (init as any)?.body : (init as any)?.body)
|
|
147
|
+
|
|
148
|
+
if (!isNullish(candidateBody)) {
|
|
149
|
+
const requestBody = _tryReadFetchBody({
|
|
150
|
+
body: candidateBody,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
requestBody?.length &&
|
|
155
|
+
(typeof Blob !== 'undefined'
|
|
156
|
+
? new Blob([requestBody]).size <= configs.maxCapturingHttpPayloadSize
|
|
157
|
+
: requestBody.length <= configs.maxCapturingHttpPayloadSize)
|
|
158
|
+
) {
|
|
159
|
+
networkRequest.requestBody = requestBody
|
|
89
160
|
}
|
|
90
161
|
}
|
|
162
|
+
}
|
|
91
163
|
|
|
164
|
+
try {
|
|
92
165
|
// Make the actual fetch request
|
|
93
166
|
const response = await originalFetch(input, init)
|
|
94
167
|
|
|
95
|
-
// Capture response data
|
|
168
|
+
// Capture response data
|
|
96
169
|
if (configs.recordResponseHeaders) {
|
|
97
|
-
|
|
98
|
-
networkRequest.responseHeaders = _headersToObject(response.headers)
|
|
99
|
-
} catch (error) {
|
|
100
|
-
console.warn('[Fetch Patch] Failed to capture response headers:', error)
|
|
101
|
-
}
|
|
170
|
+
networkRequest.responseHeaders = _headersToObject(response.headers)
|
|
102
171
|
}
|
|
103
172
|
|
|
104
173
|
if (configs.shouldRecordBody) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
responseBody = await clonedResponse.text()
|
|
115
|
-
} catch (cloneError) {
|
|
116
|
-
// If cloning fails, try to read from original response
|
|
117
|
-
// This is risky but we'll catch the error
|
|
118
|
-
try {
|
|
119
|
-
responseBody = await response.text()
|
|
120
|
-
// If we get here, we consumed the body, so we need to recreate the response
|
|
121
|
-
// This is a limitation - we can't both capture and preserve the body
|
|
122
|
-
console.warn('[Fetch Patch] Response body consumed for capture - user code may not be able to read it')
|
|
123
|
-
} catch (readError) {
|
|
124
|
-
console.warn('[Fetch Patch] Failed to read response body:', readError)
|
|
125
|
-
responseBody = '[Fetch] Unable to read response body'
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} else if (response.bodyUsed) {
|
|
129
|
-
responseBody = '[Fetch] Response body already consumed'
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
responseBody?.length &&
|
|
134
|
-
responseBody.length <= configs.maxCapturingHttpPayloadSize
|
|
135
|
-
) {
|
|
136
|
-
networkRequest.responseBody = responseBody
|
|
137
|
-
}
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.warn('[Fetch Patch] Failed to capture response body:', error)
|
|
174
|
+
const responseBody = await _tryReadResponseBody(response)
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
responseBody?.length &&
|
|
178
|
+
(typeof Blob !== 'undefined'
|
|
179
|
+
? new Blob([responseBody]).size <= configs.maxCapturingHttpPayloadSize
|
|
180
|
+
: responseBody.length <= configs.maxCapturingHttpPayloadSize)
|
|
181
|
+
) {
|
|
182
|
+
networkRequest.responseBody = responseBody
|
|
140
183
|
}
|
|
141
184
|
}
|
|
142
185
|
|
|
@@ -146,8 +189,13 @@ if (typeof fetch !== 'undefined' && typeof global !== 'undefined') {
|
|
|
146
189
|
|
|
147
190
|
return response
|
|
148
191
|
} catch (error) {
|
|
149
|
-
//
|
|
150
|
-
|
|
192
|
+
// Even if the fetch fails, we can still capture the request data
|
|
193
|
+
// Attach captured request data to the thrown error for downstream handling
|
|
194
|
+
// @ts-ignore
|
|
195
|
+
if (error && typeof error === 'object') {
|
|
196
|
+
// @ts-ignore
|
|
197
|
+
error.networkRequest = networkRequest
|
|
198
|
+
}
|
|
151
199
|
throw error
|
|
152
200
|
}
|
|
153
201
|
}
|