@multiplayer-app/session-recorder-react-native 0.0.1-beta.1 → 0.0.1-beta.11
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/SessionRecorderNative.podspec +29 -0
- package/android/build.gradle +32 -0
- package/copy-react-native-dist.sh +4 -10
- package/dist/components/MaskableComponent.d.ts +22 -0
- package/dist/components/MaskableComponent.js +1 -0
- package/dist/components/MaskableComponent.js.map +1 -0
- package/dist/components/MaskableTextInput.d.ts +14 -0
- package/dist/components/MaskableTextInput.js +1 -0
- package/dist/components/MaskableTextInput.js.map +1 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.d.ts +11 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -0
- package/dist/components/SessionRecorderWidget/FloatingButton.d.ts +8 -0
- package/dist/components/SessionRecorderWidget/FloatingButton.js +1 -0
- package/dist/components/SessionRecorderWidget/FloatingButton.js.map +1 -0
- package/dist/components/SessionRecorderWidget/InitialPopover.d.ts +13 -0
- package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -0
- package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -0
- package/dist/components/SessionRecorderWidget/ModalContainer.d.ts +8 -0
- package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -0
- package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.d.ts +6 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.js +1 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.js.map +1 -0
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.d.ts +5 -0
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js +1 -0
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js.map +1 -0
- package/dist/components/SessionRecorderWidget/icons.d.ts +11 -0
- package/dist/components/SessionRecorderWidget/icons.js +1 -0
- package/dist/components/SessionRecorderWidget/icons.js.map +1 -0
- package/dist/components/SessionRecorderWidget/index.d.ts +2 -0
- package/dist/components/SessionRecorderWidget/index.js +1 -0
- package/dist/components/SessionRecorderWidget/index.js.map +1 -0
- package/dist/components/SessionRecorderWidget/styles.d.ts +145 -0
- package/dist/components/SessionRecorderWidget/styles.js +1 -0
- package/dist/components/SessionRecorderWidget/styles.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/masking.js +1 -1
- package/dist/config/masking.js.map +1 -1
- package/dist/context/SessionRecorderContext.d.ts +5 -3
- package/dist/context/SessionRecorderContext.js +1 -1
- package/dist/context/SessionRecorderContext.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/native/ScreenMasking.d.ts +21 -0
- package/dist/native/ScreenMasking.js +1 -0
- package/dist/native/ScreenMasking.js.map +1 -0
- package/dist/native/SessionRecorderNative.d.ts +21 -0
- package/dist/native/SessionRecorderNative.js +1 -0
- package/dist/native/SessionRecorderNative.js.map +1 -0
- package/dist/patch/xhr.js +1 -1
- package/dist/patch/xhr.js.map +1 -1
- package/dist/recorder/screenRecorder.d.ts +1 -0
- package/dist/recorder/screenRecorder.js +1 -1
- package/dist/recorder/screenRecorder.js.map +1 -1
- package/dist/recorder/screenshotManager.d.ts +10 -0
- package/dist/recorder/screenshotManager.js +1 -0
- package/dist/recorder/screenshotManager.js.map +1 -0
- package/dist/services/screenMaskingService.d.ts +39 -0
- package/dist/services/screenMaskingService.js +1 -0
- package/dist/services/screenMaskingService.js.map +1 -0
- package/dist/services/storage.service.d.ts +18 -2
- package/dist/services/storage.service.js +1 -1
- package/dist/services/storage.service.js.map +1 -1
- package/dist/session-recorder.d.ts +2 -1
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/session-recorder.d.ts +6 -0
- package/dist/types/session-recorder.js.map +1 -1
- package/dist/utils/componentRegistry.d.ts +64 -0
- package/dist/utils/componentRegistry.js +1 -0
- package/dist/utils/componentRegistry.js.map +1 -0
- package/dist/utils/nativeModuleTest.d.ts +8 -0
- package/dist/utils/nativeModuleTest.js +1 -0
- package/dist/utils/nativeModuleTest.js.map +1 -0
- package/dist/utils/platform.d.ts +3 -0
- package/dist/utils/reactNativeHierarchyExtractor.d.ts +38 -0
- package/dist/utils/reactNativeHierarchyExtractor.js +1 -0
- package/dist/utils/reactNativeHierarchyExtractor.js.map +1 -0
- package/dist/utils/screenshotMasker.d.ts +96 -0
- package/dist/utils/screenshotMasker.js +1 -0
- package/dist/utils/screenshotMasker.js.map +1 -0
- package/dist/utils/viewHierarchyTracker.d.ts +89 -0
- package/dist/utils/viewHierarchyTracker.js +1 -0
- package/dist/utils/viewHierarchyTracker.js.map +1 -0
- package/ios/SessionRecorderNative.m +17 -0
- package/ios/SessionRecorderNative.podspec +26 -0
- package/ios/SessionRecorderNative.swift +205 -0
- package/package.json +22 -7
- package/react-native.config.js +15 -0
- package/RRWEB_INTEGRATION.md +0 -336
- package/VIEWSHOT_INTEGRATION_TEST.md +0 -123
- package/babel.config.js +0 -13
- package/dist/components/GestureCaptureWrapper.d.ts +0 -6
- package/dist/components/GestureCaptureWrapper.js +0 -1
- package/dist/components/GestureCaptureWrapper.js.map +0 -1
- package/dist/expo.d.ts +0 -7
- package/dist/expo.js +0 -1
- package/dist/expo.js.map +0 -1
- package/dist/otel/instrumentations/gestureInstrumentation.d.ts +0 -15
- package/dist/otel/instrumentations/gestureInstrumentation.js +0 -1
- package/dist/otel/instrumentations/gestureInstrumentation.js.map +0 -1
- package/dist/otel/instrumentations/reactNativeInstrumentation.d.ts +0 -8
- package/dist/otel/instrumentations/reactNativeInstrumentation.js +0 -1
- package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +0 -1
- package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +0 -13
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js +0 -1
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +0 -1
- package/dist/recorder/gestureHandlerRecorder.d.ts +0 -19
- package/dist/recorder/gestureHandlerRecorder.js +0 -1
- package/dist/recorder/gestureHandlerRecorder.js.map +0 -1
- package/dist/types/rrweb.d.ts +0 -118
- package/dist/types/rrweb.js +0 -1
- package/dist/types/rrweb.js.map +0 -1
- package/scripts/generate-app-metadata.js +0 -173
- package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +0 -86
- package/src/components/GestureCaptureWrapper/index.ts +0 -1
- package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +0 -72
- package/src/components/ScreenRecorderView/index.ts +0 -1
- package/src/components/index.ts +0 -1
- package/src/config/constants.ts +0 -60
- package/src/config/defaults.ts +0 -82
- package/src/config/index.ts +0 -6
- package/src/config/masking.ts +0 -27
- package/src/config/session-recorder.ts +0 -55
- package/src/config/validators.ts +0 -31
- package/src/context/SessionRecorderContext.tsx +0 -75
- package/src/expo.ts +0 -11
- package/src/index.ts +0 -17
- package/src/otel/helpers.ts +0 -275
- package/src/otel/index.ts +0 -138
- package/src/otel/instrumentations/index.ts +0 -115
- package/src/patch/index.ts +0 -1
- package/src/patch/xhr.ts +0 -142
- package/src/recorder/eventExporter.ts +0 -141
- package/src/recorder/gestureRecorder.ts +0 -498
- package/src/recorder/index.ts +0 -179
- package/src/recorder/navigationTracker.ts +0 -449
- package/src/recorder/screenRecorder.ts +0 -498
- package/src/services/api.service.ts +0 -203
- package/src/services/storage.service.ts +0 -158
- package/src/session-recorder.ts +0 -600
- package/src/types/expo.d.ts +0 -23
- package/src/types/index.ts +0 -28
- package/src/types/session-recorder.ts +0 -423
- package/src/types/session.ts +0 -65
- package/src/utils/app-metadata.ts +0 -31
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.ts +0 -225
- package/src/utils/platform.ts +0 -384
- package/src/utils/request-utils.ts +0 -61
- package/src/utils/rrweb-events.ts +0 -309
- package/src/utils/session.ts +0 -18
- package/src/utils/time.ts +0 -17
- package/src/utils/type-utils.ts +0 -75
- package/src/version.ts +0 -1
- package/tsconfig.json +0 -24
package/src/otel/helpers.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { Span } from '@opentelemetry/api'
|
|
2
|
-
import {
|
|
3
|
-
MULTIPLAYER_TRACE_DEBUG_PREFIX,
|
|
4
|
-
MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX,
|
|
5
|
-
ATTR_MULTIPLAYER_HTTP_REQUEST_BODY,
|
|
6
|
-
ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS,
|
|
7
|
-
ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY,
|
|
8
|
-
ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS,
|
|
9
|
-
} from '@multiplayer-app/session-recorder-common'
|
|
10
|
-
import { logger } from '../utils'
|
|
11
|
-
import { SessionRecorderSdk } from '@multiplayer-app/session-recorder-common'
|
|
12
|
-
import { TracerReactNativeConfig } from '../types'
|
|
13
|
-
|
|
14
|
-
const { schemify } = SessionRecorderSdk
|
|
15
|
-
|
|
16
|
-
export interface HttpPayloadData {
|
|
17
|
-
requestBody?: any
|
|
18
|
-
responseBody?: any
|
|
19
|
-
requestHeaders?: Record<string, string>
|
|
20
|
-
responseHeaders?: Record<string, string>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ProcessedHttpPayload {
|
|
24
|
-
requestBody?: string
|
|
25
|
-
responseBody?: string
|
|
26
|
-
requestHeaders?: string
|
|
27
|
-
responseHeaders?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Checks if the trace should be processed based on trace ID prefixes
|
|
32
|
-
*/
|
|
33
|
-
export function shouldProcessTrace(traceId: string): boolean {
|
|
34
|
-
return (
|
|
35
|
-
traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
|
36
|
-
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Processes request and response body based on trace type and configuration
|
|
42
|
-
*/
|
|
43
|
-
export function processBody(
|
|
44
|
-
payload: HttpPayloadData,
|
|
45
|
-
config: TracerReactNativeConfig,
|
|
46
|
-
span: Span,
|
|
47
|
-
): { requestBody?: string; responseBody?: string } {
|
|
48
|
-
const { captureBody, masking } = config
|
|
49
|
-
const traceId = span.spanContext().traceId
|
|
50
|
-
|
|
51
|
-
if (!captureBody) {
|
|
52
|
-
return {}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let { requestBody, responseBody } = payload
|
|
56
|
-
|
|
57
|
-
if (requestBody !== undefined && requestBody !== null) {
|
|
58
|
-
requestBody = JSON.parse(JSON.stringify(requestBody))
|
|
59
|
-
}
|
|
60
|
-
if (responseBody !== undefined && responseBody !== null) {
|
|
61
|
-
responseBody = JSON.parse(JSON.stringify(responseBody))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Apply masking for debug traces
|
|
65
|
-
if (
|
|
66
|
-
traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
|
67
|
-
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
|
68
|
-
) {
|
|
69
|
-
if (masking.isContentMaskingEnabled) {
|
|
70
|
-
requestBody = requestBody && masking.maskBody?.(requestBody, span)
|
|
71
|
-
responseBody = responseBody && masking.maskBody?.(responseBody, span)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Convert to string if needed
|
|
76
|
-
if (typeof requestBody !== 'string') {
|
|
77
|
-
requestBody = JSON.stringify(requestBody)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (typeof responseBody !== 'string') {
|
|
81
|
-
responseBody = JSON.stringify(responseBody)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
requestBody: requestBody?.length ? requestBody : undefined,
|
|
86
|
-
responseBody: responseBody?.length ? responseBody : undefined,
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Processes request and response headers based on configuration
|
|
92
|
-
*/
|
|
93
|
-
export function processHeaders(
|
|
94
|
-
payload: HttpPayloadData,
|
|
95
|
-
config: TracerReactNativeConfig,
|
|
96
|
-
span: Span,
|
|
97
|
-
): { requestHeaders?: string; responseHeaders?: string } {
|
|
98
|
-
const { captureHeaders, masking } = config
|
|
99
|
-
|
|
100
|
-
if (!captureHeaders) {
|
|
101
|
-
return {}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let { requestHeaders = {}, responseHeaders = {} } = payload
|
|
105
|
-
|
|
106
|
-
// Handle header filtering
|
|
107
|
-
if (
|
|
108
|
-
!masking.headersToInclude?.length &&
|
|
109
|
-
!masking.headersToExclude?.length
|
|
110
|
-
) {
|
|
111
|
-
// Add null checks to prevent JSON.parse error when headers is undefined
|
|
112
|
-
if (requestHeaders !== undefined && requestHeaders !== null) {
|
|
113
|
-
requestHeaders = JSON.parse(JSON.stringify(requestHeaders))
|
|
114
|
-
}
|
|
115
|
-
if (responseHeaders !== undefined && responseHeaders !== null) {
|
|
116
|
-
responseHeaders = JSON.parse(JSON.stringify(responseHeaders))
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
if (masking.headersToInclude) {
|
|
120
|
-
const _requestHeaders: Record<string, string> = {}
|
|
121
|
-
const _responseHeaders: Record<string, string> = {}
|
|
122
|
-
|
|
123
|
-
for (const headerName of masking.headersToInclude) {
|
|
124
|
-
if (requestHeaders[headerName]) {
|
|
125
|
-
_requestHeaders[headerName] = requestHeaders[headerName]
|
|
126
|
-
}
|
|
127
|
-
if (responseHeaders[headerName]) {
|
|
128
|
-
_responseHeaders[headerName] = responseHeaders[headerName]
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
requestHeaders = _requestHeaders
|
|
133
|
-
responseHeaders = _responseHeaders
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (masking.headersToExclude?.length) {
|
|
137
|
-
for (const headerName of masking.headersToExclude) {
|
|
138
|
-
delete requestHeaders[headerName]
|
|
139
|
-
delete responseHeaders[headerName]
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Apply masking
|
|
145
|
-
const maskedRequestHeaders = masking.maskHeaders?.(requestHeaders, span) || requestHeaders
|
|
146
|
-
const maskedResponseHeaders = masking.maskHeaders?.(responseHeaders, span) || responseHeaders
|
|
147
|
-
|
|
148
|
-
// Convert to string
|
|
149
|
-
const requestHeadersStr = typeof maskedRequestHeaders === 'string'
|
|
150
|
-
? maskedRequestHeaders
|
|
151
|
-
: JSON.stringify(maskedRequestHeaders)
|
|
152
|
-
|
|
153
|
-
const responseHeadersStr = typeof maskedResponseHeaders === 'string'
|
|
154
|
-
? maskedResponseHeaders
|
|
155
|
-
: JSON.stringify(maskedResponseHeaders)
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
requestHeaders: requestHeadersStr?.length ? requestHeadersStr : undefined,
|
|
159
|
-
responseHeaders: responseHeadersStr?.length ? responseHeadersStr : undefined,
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Processes HTTP payload (body and headers) and sets span attributes
|
|
165
|
-
*/
|
|
166
|
-
export function processHttpPayload(
|
|
167
|
-
payload: HttpPayloadData,
|
|
168
|
-
config: TracerReactNativeConfig,
|
|
169
|
-
span: Span,
|
|
170
|
-
): void {
|
|
171
|
-
const traceId = span.spanContext().traceId
|
|
172
|
-
|
|
173
|
-
if (!shouldProcessTrace(traceId)) {
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const { requestBody, responseBody } = processBody(payload, config, span)
|
|
178
|
-
const { requestHeaders, responseHeaders } = processHeaders(payload, config, span)
|
|
179
|
-
|
|
180
|
-
// Set span attributes
|
|
181
|
-
if (requestBody) {
|
|
182
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_BODY, requestBody)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (responseBody) {
|
|
186
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY, responseBody)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (requestHeaders) {
|
|
190
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS, requestHeaders)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (responseHeaders) {
|
|
194
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, responseHeaders)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Converts Headers object to plain object
|
|
200
|
-
*/
|
|
201
|
-
export function headersToObject(headers: Headers | Record<string, string> | Record<string, string | string[]> | string[][] | undefined): Record<string, string> {
|
|
202
|
-
const result: Record<string, string> = {}
|
|
203
|
-
|
|
204
|
-
if (!headers) {
|
|
205
|
-
return result
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (headers instanceof Headers) {
|
|
209
|
-
headers.forEach((value: string, key: string) => {
|
|
210
|
-
result[key] = value
|
|
211
|
-
})
|
|
212
|
-
} else if (Array.isArray(headers)) {
|
|
213
|
-
// Handle array of [key, value] pairs
|
|
214
|
-
for (const [key, value] of headers) {
|
|
215
|
-
if (typeof key === 'string' && typeof value === 'string') {
|
|
216
|
-
result[key] = value
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} else if (typeof headers === 'object' && !Array.isArray(headers)) {
|
|
220
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
221
|
-
if (typeof key === 'string' && typeof value === 'string') {
|
|
222
|
-
result[key] = value
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return result
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Extracts response body as string from Response object
|
|
232
|
-
*/
|
|
233
|
-
export async function extractResponseBody(response: Response): Promise<string | null> {
|
|
234
|
-
if (!response.body) {
|
|
235
|
-
return null
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
if (response.body instanceof ReadableStream) {
|
|
240
|
-
// Check if response body is already consumed
|
|
241
|
-
if (response.bodyUsed) {
|
|
242
|
-
return null
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const responseClone = response.clone()
|
|
246
|
-
return responseClone.text()
|
|
247
|
-
} else {
|
|
248
|
-
return JSON.stringify(response.body)
|
|
249
|
-
}
|
|
250
|
-
} catch (error) {
|
|
251
|
-
// If cloning fails (body already consumed), return null
|
|
252
|
-
// eslint-disable-next-line no-console
|
|
253
|
-
logger.warn('DEBUGGER_LIB', 'Failed to extract response body', error)
|
|
254
|
-
return null
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export const getExporterEndpoint = (exporterEndpoint: string): string => {
|
|
259
|
-
const hasPath = exporterEndpoint && (() => {
|
|
260
|
-
try {
|
|
261
|
-
const url = new URL(exporterEndpoint)
|
|
262
|
-
return url.pathname !== '/' && url.pathname !== ''
|
|
263
|
-
} catch {
|
|
264
|
-
return false
|
|
265
|
-
}
|
|
266
|
-
})()
|
|
267
|
-
|
|
268
|
-
if (hasPath) {
|
|
269
|
-
return exporterEndpoint
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const trimmedExporterEndpoint = new URL(exporterEndpoint).origin
|
|
273
|
-
|
|
274
|
-
return `${trimmedExporterEndpoint}/v1/traces`
|
|
275
|
-
}
|
package/src/otel/index.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { resourceFromAttributes } from '@opentelemetry/resources'
|
|
2
|
-
import { W3CTraceContextPropagator } from '@opentelemetry/core'
|
|
3
|
-
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
4
|
-
import * as SemanticAttributes from '@opentelemetry/semantic-conventions'
|
|
5
|
-
import { registerInstrumentations } from '@opentelemetry/instrumentation'
|
|
6
|
-
import {
|
|
7
|
-
SessionType,
|
|
8
|
-
ATTR_MULTIPLAYER_SESSION_ID,
|
|
9
|
-
SessionRecorderIdGenerator,
|
|
10
|
-
SessionRecorderTraceIdRatioBasedSampler,
|
|
11
|
-
SessionRecorderBrowserTraceExporter,
|
|
12
|
-
} from '@multiplayer-app/session-recorder-common'
|
|
13
|
-
import { TracerReactNativeConfig } from '../types'
|
|
14
|
-
import { getInstrumentations } from './instrumentations'
|
|
15
|
-
import { getExporterEndpoint } from './helpers'
|
|
16
|
-
|
|
17
|
-
import { getPlatformAttributes } from '../utils/platform'
|
|
18
|
-
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export class TracerReactNativeSDK {
|
|
24
|
-
private tracerProvider?: WebTracerProvider
|
|
25
|
-
private config?: TracerReactNativeConfig
|
|
26
|
-
|
|
27
|
-
private sessionId = ''
|
|
28
|
-
private idGenerator?: SessionRecorderIdGenerator
|
|
29
|
-
private exporter?: any
|
|
30
|
-
private isInitialized = false
|
|
31
|
-
|
|
32
|
-
constructor() { }
|
|
33
|
-
|
|
34
|
-
private _setSessionId(
|
|
35
|
-
sessionId: string,
|
|
36
|
-
sessionType: SessionType = SessionType.PLAIN,
|
|
37
|
-
) {
|
|
38
|
-
this.sessionId = sessionId
|
|
39
|
-
this.idGenerator?.setSessionId(sessionId, sessionType)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
init(options: TracerReactNativeConfig): void {
|
|
43
|
-
this.config = options
|
|
44
|
-
|
|
45
|
-
const { application, version, environment } = this.config
|
|
46
|
-
|
|
47
|
-
this.idGenerator = new SessionRecorderIdGenerator()
|
|
48
|
-
|
|
49
|
-
this.exporter = new SessionRecorderBrowserTraceExporter({
|
|
50
|
-
apiKey: options.apiKey,
|
|
51
|
-
url: getExporterEndpoint(options.exporterEndpoint),
|
|
52
|
-
usePostMessageFallback: options.usePostMessageFallback,
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
this.tracerProvider = new WebTracerProvider({
|
|
56
|
-
resource: resourceFromAttributes({
|
|
57
|
-
[SemanticAttributes.SEMRESATTRS_SERVICE_NAME]: application,
|
|
58
|
-
[SemanticAttributes.SEMRESATTRS_SERVICE_VERSION]: version,
|
|
59
|
-
[SemanticAttributes.SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: environment,
|
|
60
|
-
...getPlatformAttributes(),
|
|
61
|
-
}),
|
|
62
|
-
idGenerator: this.idGenerator,
|
|
63
|
-
sampler: new SessionRecorderTraceIdRatioBasedSampler(this.config.sampleTraceRatio || 0.15),
|
|
64
|
-
spanProcessors: [
|
|
65
|
-
this._getSpanSessionIdProcessor(),
|
|
66
|
-
new BatchSpanProcessor(this.exporter),
|
|
67
|
-
],
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
this.tracerProvider.register({
|
|
71
|
-
propagator: new W3CTraceContextPropagator(),
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// Register instrumentations
|
|
75
|
-
registerInstrumentations({
|
|
76
|
-
tracerProvider: this.tracerProvider,
|
|
77
|
-
instrumentations: getInstrumentations(this.config),
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
this.isInitialized = true
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private _getSpanSessionIdProcessor() {
|
|
84
|
-
return {
|
|
85
|
-
onStart: (span: any) => {
|
|
86
|
-
if (this.sessionId) {
|
|
87
|
-
span.setAttribute(ATTR_MULTIPLAYER_SESSION_ID, this.sessionId)
|
|
88
|
-
}
|
|
89
|
-
// Add React Native specific attributes
|
|
90
|
-
span.setAttribute('platform', 'react-native')
|
|
91
|
-
span.setAttribute('timestamp', Date.now())
|
|
92
|
-
},
|
|
93
|
-
onEnd: () => { },
|
|
94
|
-
shutdown: () => Promise.resolve(),
|
|
95
|
-
forceFlush: () => Promise.resolve(),
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
start(sessionId: string, sessionType: SessionType): void {
|
|
100
|
-
if (!this.tracerProvider) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
'Configuration not initialized. Call init() before start().',
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this._setSessionId(sessionId, sessionType)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
stop(): void {
|
|
110
|
-
if (!this.tracerProvider) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
'Configuration not initialized. Call init() before start().',
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
this._setSessionId('')
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
setApiKey(apiKey: string): void {
|
|
120
|
-
if (!this.exporter) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
'Configuration not initialized. Call init() before setApiKey().',
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
this.exporter.setApiKey?.(apiKey)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
setSessionId(sessionId: string, sessionType: SessionType): void {
|
|
130
|
-
this._setSessionId(sessionId, sessionType)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Shutdown (React Native specific)
|
|
134
|
-
shutdown(): Promise<void> {
|
|
135
|
-
this.isInitialized = false
|
|
136
|
-
return Promise.resolve()
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'
|
|
2
|
-
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'
|
|
3
|
-
|
|
4
|
-
import { logger } from '../../utils'
|
|
5
|
-
import { OTEL_IGNORE_URLS } from '../../config'
|
|
6
|
-
import { TracerReactNativeConfig } from '../../types'
|
|
7
|
-
import { extractResponseBody, headersToObject, processHttpPayload } from '../helpers'
|
|
8
|
-
|
|
9
|
-
export function getInstrumentations(config: TracerReactNativeConfig) {
|
|
10
|
-
|
|
11
|
-
const instrumentations = []
|
|
12
|
-
|
|
13
|
-
// Fetch instrumentation
|
|
14
|
-
try {
|
|
15
|
-
instrumentations.push(
|
|
16
|
-
new FetchInstrumentation({
|
|
17
|
-
clearTimingResources: false,
|
|
18
|
-
ignoreUrls: [
|
|
19
|
-
...OTEL_IGNORE_URLS,
|
|
20
|
-
...(config.ignoreUrls || []),
|
|
21
|
-
],
|
|
22
|
-
propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls,
|
|
23
|
-
applyCustomAttributesOnSpan: async (span, request, response) => {
|
|
24
|
-
if (!config) return
|
|
25
|
-
|
|
26
|
-
const { captureBody, captureHeaders } = config
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
if (!captureBody && !captureHeaders) {
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const requestBody = request.body
|
|
34
|
-
const requestHeaders = headersToObject(request.headers)
|
|
35
|
-
const responseHeaders = headersToObject(response instanceof Response ? response.headers : undefined)
|
|
36
|
-
|
|
37
|
-
let responseBody: string | null = null
|
|
38
|
-
if (response instanceof Response && response.body) {
|
|
39
|
-
responseBody = await extractResponseBody(response)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const payload = {
|
|
43
|
-
requestBody,
|
|
44
|
-
responseBody,
|
|
45
|
-
requestHeaders,
|
|
46
|
-
responseHeaders,
|
|
47
|
-
}
|
|
48
|
-
processHttpPayload(payload, config, span)
|
|
49
|
-
} catch (error) {
|
|
50
|
-
// eslint-disable-next-line
|
|
51
|
-
logger.error('DEBUGGER_LIB', 'Failed to capture fetch payload', error)
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
})
|
|
55
|
-
)
|
|
56
|
-
} catch (error) {
|
|
57
|
-
logger.warn('DEBUGGER_LIB', 'Fetch instrumentation not available', error)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// XMLHttpRequest instrumentation
|
|
61
|
-
try {
|
|
62
|
-
instrumentations.push(
|
|
63
|
-
new XMLHttpRequestInstrumentation({
|
|
64
|
-
clearTimingResources: false,
|
|
65
|
-
ignoreUrls: [
|
|
66
|
-
...OTEL_IGNORE_URLS,
|
|
67
|
-
...(config.ignoreUrls || []),
|
|
68
|
-
],
|
|
69
|
-
propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls,
|
|
70
|
-
applyCustomAttributesOnSpan: (span, xhr) => {
|
|
71
|
-
if (!config) return
|
|
72
|
-
|
|
73
|
-
const { captureBody, captureHeaders } = config
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
if (!captureBody && !captureHeaders) {
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// @ts-ignore
|
|
81
|
-
const requestBody = xhr.networkRequest.requestBody
|
|
82
|
-
// @ts-ignore
|
|
83
|
-
const responseBody = xhr.networkRequest.responseBody
|
|
84
|
-
// @ts-ignore
|
|
85
|
-
const requestHeaders = xhr.networkRequest.requestHeaders || {}
|
|
86
|
-
// @ts-ignore
|
|
87
|
-
const responseHeaders = xhr.networkRequest.responseHeaders || {}
|
|
88
|
-
|
|
89
|
-
const payload = {
|
|
90
|
-
requestBody,
|
|
91
|
-
responseBody,
|
|
92
|
-
requestHeaders,
|
|
93
|
-
responseHeaders,
|
|
94
|
-
}
|
|
95
|
-
processHttpPayload(payload, config, span)
|
|
96
|
-
} catch (error) {
|
|
97
|
-
// eslint-disable-next-line
|
|
98
|
-
logger.error('DEBUGGER_LIB', 'Failed to capture xml-http payload', error)
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
})
|
|
102
|
-
)
|
|
103
|
-
} catch (error) {
|
|
104
|
-
logger.warn('DEBUGGER_LIB', 'XMLHttpRequest instrumentation not available', error)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Custom React Native instrumentations
|
|
108
|
-
// try {
|
|
109
|
-
// instrumentations.push(new ReactNativeInstrumentation())
|
|
110
|
-
// } catch (error) {
|
|
111
|
-
// console.warn('React Native instrumentation not available:', error)
|
|
112
|
-
// }
|
|
113
|
-
|
|
114
|
-
return instrumentations
|
|
115
|
-
}
|
package/src/patch/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import './xhr'
|
package/src/patch/xhr.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
isFormData,
|
|
4
|
-
isNullish,
|
|
5
|
-
isObject,
|
|
6
|
-
isString,
|
|
7
|
-
} from '../utils/type-utils'
|
|
8
|
-
import { formDataToQuery } from '../utils/request-utils'
|
|
9
|
-
import { DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE } from '../config'
|
|
10
|
-
|
|
11
|
-
let recordRequestHeaders = true
|
|
12
|
-
let recordResponseHeaders = true
|
|
13
|
-
const shouldRecordBody = true
|
|
14
|
-
let maxCapturingHttpPayloadSize = DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE
|
|
15
|
-
|
|
16
|
-
export const setMaxCapturingHttpPayloadSize = (_maxCapturingHttpPayloadSize: number) => {
|
|
17
|
-
maxCapturingHttpPayloadSize = _maxCapturingHttpPayloadSize
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const setShouldRecordHttpData = (shouldRecordBody: boolean, shouldRecordHeaders: boolean) => {
|
|
21
|
-
recordRequestHeaders = shouldRecordHeaders
|
|
22
|
-
recordResponseHeaders = shouldRecordHeaders
|
|
23
|
-
// eslint-disable-next-line
|
|
24
|
-
shouldRecordBody = shouldRecordBody
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function _tryReadXHRBody({
|
|
28
|
-
body,
|
|
29
|
-
url,
|
|
30
|
-
}: {
|
|
31
|
-
body: any | null | undefined
|
|
32
|
-
url: string | URL | RequestInfo
|
|
33
|
-
}): string | null {
|
|
34
|
-
if (isNullish(body)) {
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (isString(body)) {
|
|
39
|
-
return body
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (isFormData(body)) {
|
|
44
|
-
return formDataToQuery(body)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (isObject(body)) {
|
|
48
|
-
try {
|
|
49
|
-
return JSON.stringify(body)
|
|
50
|
-
} catch {
|
|
51
|
-
return '[XHR] Failed to stringify response object'
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return `[XHR] Cannot read body of type ${Object.prototype.toString.call(body)}`
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
(function (xhr) {
|
|
59
|
-
const originalOpen = XMLHttpRequest.prototype.open
|
|
60
|
-
|
|
61
|
-
xhr.open = function (
|
|
62
|
-
method: string,
|
|
63
|
-
url: string | URL,
|
|
64
|
-
async = true,
|
|
65
|
-
username?: string | null,
|
|
66
|
-
password?: string | null,
|
|
67
|
-
) {
|
|
68
|
-
const xhr = this as XMLHttpRequest
|
|
69
|
-
const networkRequest: {
|
|
70
|
-
requestHeaders?: any,
|
|
71
|
-
requestBody?: any,
|
|
72
|
-
responseHeaders?: any,
|
|
73
|
-
responseBody?: any,
|
|
74
|
-
} = {}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// @ts-ignore
|
|
78
|
-
const requestHeaders: Record<string, string> = {}
|
|
79
|
-
const originalSetRequestHeader = xhr.setRequestHeader.bind(xhr)
|
|
80
|
-
xhr.setRequestHeader = (header: string, value: string) => {
|
|
81
|
-
requestHeaders[header] = value
|
|
82
|
-
return originalSetRequestHeader(header, value)
|
|
83
|
-
}
|
|
84
|
-
if (recordRequestHeaders) {
|
|
85
|
-
networkRequest.requestHeaders = requestHeaders
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const originalSend = xhr.send.bind(xhr)
|
|
89
|
-
xhr.send = (body) => {
|
|
90
|
-
if (shouldRecordBody) {
|
|
91
|
-
const requestBody = _tryReadXHRBody({ body, url })
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
requestBody?.length
|
|
95
|
-
&& requestBody.length <= maxCapturingHttpPayloadSize
|
|
96
|
-
) {
|
|
97
|
-
networkRequest.requestBody = requestBody
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return originalSend(body)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
xhr.addEventListener('readystatechange', () => {
|
|
104
|
-
if (xhr.readyState !== xhr.DONE) {
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// @ts-ignore
|
|
110
|
-
const responseHeaders: Record<string, string> = {}
|
|
111
|
-
const rawHeaders = xhr.getAllResponseHeaders()
|
|
112
|
-
const headers = rawHeaders.trim().split(/[\r\n]+/)
|
|
113
|
-
headers.forEach((line) => {
|
|
114
|
-
const parts = line.split(': ')
|
|
115
|
-
const header = parts.shift()
|
|
116
|
-
const value = parts.join(': ')
|
|
117
|
-
if (header) {
|
|
118
|
-
responseHeaders[header] = value
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
if (recordResponseHeaders) {
|
|
122
|
-
networkRequest.responseHeaders = responseHeaders
|
|
123
|
-
}
|
|
124
|
-
if (shouldRecordBody) {
|
|
125
|
-
const responseBody = _tryReadXHRBody({ body: xhr.response, url })
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
responseBody?.length
|
|
129
|
-
&& responseBody.length <= maxCapturingHttpPayloadSize
|
|
130
|
-
) {
|
|
131
|
-
networkRequest.responseBody = responseBody
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// @ts-ignore
|
|
138
|
-
xhr.networkRequest = networkRequest
|
|
139
|
-
|
|
140
|
-
originalOpen.call(xhr, method, url as string, async, username, password)
|
|
141
|
-
}
|
|
142
|
-
})(XMLHttpRequest.prototype)
|