@multiplayer-app/session-recorder-react-native 0.0.1-alpha.1 → 0.0.1-alpha.10

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 (234) hide show
  1. package/RRWEB_INTEGRATION.md +336 -0
  2. package/VIEWSHOT_INTEGRATION_TEST.md +123 -0
  3. package/copy-react-native-dist.sh +38 -0
  4. package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.d.ts +6 -0
  5. package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js +1 -0
  6. package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js.map +1 -0
  7. package/dist/components/GestureCaptureWrapper/index.d.ts +1 -0
  8. package/dist/components/GestureCaptureWrapper/index.js +1 -0
  9. package/dist/components/GestureCaptureWrapper/index.js.map +1 -0
  10. package/dist/components/GestureCaptureWrapper.d.ts +6 -0
  11. package/dist/components/GestureCaptureWrapper.js +1 -0
  12. package/dist/components/GestureCaptureWrapper.js.map +1 -0
  13. package/dist/components/ScreenRecorderView/ScreenRecorderView.d.ts +5 -0
  14. package/dist/components/ScreenRecorderView/ScreenRecorderView.js +1 -0
  15. package/dist/components/ScreenRecorderView/ScreenRecorderView.js.map +1 -0
  16. package/dist/components/ScreenRecorderView/index.d.ts +1 -0
  17. package/dist/components/ScreenRecorderView/index.js +1 -0
  18. package/dist/components/ScreenRecorderView/index.js.map +1 -0
  19. package/dist/components/index.d.ts +1 -0
  20. package/dist/components/index.js +1 -0
  21. package/dist/components/index.js.map +1 -0
  22. package/dist/config/constants.d.ts +18 -0
  23. package/dist/config/constants.js +1 -0
  24. package/dist/config/constants.js.map +1 -0
  25. package/dist/config/defaults.d.ts +4 -0
  26. package/dist/config/defaults.js +1 -0
  27. package/dist/config/defaults.js.map +1 -0
  28. package/dist/config/index.d.ts +5 -0
  29. package/dist/config/index.js +1 -0
  30. package/dist/config/index.js.map +1 -0
  31. package/dist/config/masking.d.ts +2 -30
  32. package/dist/config/masking.js +1 -1
  33. package/dist/config/masking.js.map +1 -1
  34. package/dist/config/session-recorder.d.ts +2 -0
  35. package/dist/config/session-recorder.js +1 -0
  36. package/dist/config/session-recorder.js.map +1 -0
  37. package/dist/config/validators.d.ts +10 -0
  38. package/dist/config/validators.js +1 -0
  39. package/dist/config/validators.js.map +1 -0
  40. package/dist/context/SessionRecorderContext.d.ts +12 -0
  41. package/dist/context/SessionRecorderContext.js +1 -0
  42. package/dist/context/SessionRecorderContext.js.map +1 -0
  43. package/dist/expo.d.ts +5 -9
  44. package/dist/expo.js +1 -1
  45. package/dist/expo.js.map +1 -1
  46. package/dist/index.d.ts +6 -10
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/otel/helpers.d.ts +45 -3
  50. package/dist/otel/helpers.js +1 -1
  51. package/dist/otel/helpers.js.map +1 -1
  52. package/dist/otel/index.d.ts +4 -25
  53. package/dist/otel/index.js +1 -1
  54. package/dist/otel/index.js.map +1 -1
  55. package/dist/otel/instrumentations/gestureInstrumentation.js +1 -1
  56. package/dist/otel/instrumentations/gestureInstrumentation.js.map +1 -1
  57. package/dist/otel/instrumentations/index.d.ts +3 -4
  58. package/dist/otel/instrumentations/index.js +1 -1
  59. package/dist/otel/instrumentations/index.js.map +1 -1
  60. package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -1
  61. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -1
  62. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +1 -0
  63. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -1
  64. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -1
  65. package/dist/patch/index.d.ts +1 -0
  66. package/dist/patch/index.js +1 -0
  67. package/dist/patch/index.js.map +1 -0
  68. package/dist/patch/xhr.d.ts +2 -0
  69. package/dist/patch/xhr.js +1 -0
  70. package/dist/patch/xhr.js.map +1 -0
  71. package/dist/recorder/eventExporter.d.ts +21 -0
  72. package/dist/recorder/eventExporter.js +1 -0
  73. package/dist/recorder/eventExporter.js.map +1 -0
  74. package/dist/recorder/gestureHandlerRecorder.d.ts +19 -0
  75. package/dist/recorder/gestureHandlerRecorder.js +1 -0
  76. package/dist/recorder/gestureHandlerRecorder.js.map +1 -0
  77. package/dist/recorder/gestureRecorder.d.ts +68 -11
  78. package/dist/recorder/gestureRecorder.js +1 -1
  79. package/dist/recorder/gestureRecorder.js.map +1 -1
  80. package/dist/recorder/index.d.ts +60 -6
  81. package/dist/recorder/index.js +1 -1
  82. package/dist/recorder/index.js.map +1 -1
  83. package/dist/recorder/navigationTracker.js +1 -1
  84. package/dist/recorder/navigationTracker.js.map +1 -1
  85. package/dist/recorder/screenRecorder.d.ts +79 -10
  86. package/dist/recorder/screenRecorder.js +1 -1
  87. package/dist/recorder/screenRecorder.js.map +1 -1
  88. package/dist/services/api.service.d.ts +62 -10
  89. package/dist/services/api.service.js +1 -1
  90. package/dist/services/api.service.js.map +1 -1
  91. package/dist/services/storage.service.d.ts +23 -16
  92. package/dist/services/storage.service.js +1 -1
  93. package/dist/services/storage.service.js.map +1 -1
  94. package/dist/session-recorder.d.ts +166 -0
  95. package/dist/session-recorder.js +1 -0
  96. package/dist/session-recorder.js.map +1 -0
  97. package/dist/types/index.d.ts +15 -76
  98. package/dist/types/index.js +1 -1
  99. package/dist/types/index.js.map +1 -1
  100. package/dist/types/rrweb.d.ts +118 -0
  101. package/dist/types/rrweb.js +1 -0
  102. package/dist/types/rrweb.js.map +1 -0
  103. package/dist/types/session-recorder.d.ts +366 -0
  104. package/dist/types/session-recorder.js +1 -0
  105. package/dist/types/session-recorder.js.map +1 -0
  106. package/dist/types/session.d.ts +59 -0
  107. package/dist/types/session.js +1 -0
  108. package/dist/types/session.js.map +1 -0
  109. package/dist/utils/app-metadata.d.ts +16 -0
  110. package/dist/utils/app-metadata.js +1 -0
  111. package/dist/utils/app-metadata.js.map +1 -0
  112. package/dist/utils/index.d.ts +7 -0
  113. package/dist/utils/index.js +1 -0
  114. package/dist/utils/index.js.map +1 -0
  115. package/dist/utils/logger.d.ts +112 -0
  116. package/dist/utils/logger.js +1 -0
  117. package/dist/utils/logger.js.map +1 -0
  118. package/dist/utils/platform.d.ts +37 -0
  119. package/dist/utils/platform.js +1 -1
  120. package/dist/utils/platform.js.map +1 -1
  121. package/dist/utils/request-utils.d.ts +21 -0
  122. package/dist/utils/request-utils.js +1 -0
  123. package/dist/utils/request-utils.js.map +1 -0
  124. package/dist/utils/rrweb-events.d.ts +65 -0
  125. package/dist/utils/rrweb-events.js +1 -0
  126. package/dist/utils/rrweb-events.js.map +1 -0
  127. package/dist/utils/session.d.ts +5 -0
  128. package/dist/utils/session.js +1 -0
  129. package/dist/utils/session.js.map +1 -0
  130. package/dist/utils/time.d.ts +4 -0
  131. package/dist/utils/time.js +1 -0
  132. package/dist/utils/time.js.map +1 -0
  133. package/dist/utils/type-utils.d.ts +16 -0
  134. package/dist/utils/type-utils.js +1 -0
  135. package/dist/utils/type-utils.js.map +1 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/dist/version.js.map +1 -1
  139. package/docs/AUTO_METADATA_DETECTION.md +108 -0
  140. package/package.json +10 -9
  141. package/scripts/generate-app-metadata.js +173 -0
  142. package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +86 -0
  143. package/src/components/GestureCaptureWrapper/index.ts +1 -0
  144. package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +72 -0
  145. package/src/components/ScreenRecorderView/index.ts +1 -0
  146. package/src/components/index.ts +1 -0
  147. package/src/config/constants.ts +60 -0
  148. package/src/config/defaults.ts +82 -0
  149. package/src/config/index.ts +6 -0
  150. package/src/config/masking.ts +10 -61
  151. package/src/config/session-recorder.ts +55 -0
  152. package/src/config/validators.ts +31 -0
  153. package/src/context/SessionRecorderContext.tsx +75 -0
  154. package/src/expo.ts +7 -37
  155. package/src/index.ts +14 -17
  156. package/src/otel/helpers.ts +265 -11
  157. package/src/otel/index.ts +37 -247
  158. package/src/otel/instrumentations/index.ts +82 -53
  159. package/src/patch/index.ts +1 -0
  160. package/src/patch/xhr.ts +142 -0
  161. package/src/recorder/eventExporter.ts +141 -0
  162. package/src/recorder/gestureRecorder.ts +194 -125
  163. package/src/recorder/index.ts +132 -24
  164. package/src/recorder/navigationTracker.ts +12 -10
  165. package/src/recorder/screenRecorder.ts +242 -155
  166. package/src/services/api.service.ts +170 -45
  167. package/src/services/storage.service.ts +102 -74
  168. package/src/session-recorder.ts +600 -0
  169. package/src/types/index.ts +19 -79
  170. package/src/types/session-recorder.ts +423 -0
  171. package/src/types/session.ts +65 -0
  172. package/src/utils/app-metadata.ts +31 -0
  173. package/src/utils/index.ts +8 -0
  174. package/src/utils/logger.ts +225 -0
  175. package/src/utils/platform.ts +321 -6
  176. package/src/utils/request-utils.ts +61 -0
  177. package/src/utils/rrweb-events.ts +309 -0
  178. package/src/utils/session.ts +18 -0
  179. package/src/utils/time.ts +17 -0
  180. package/src/utils/type-utils.ts +75 -0
  181. package/src/version.ts +1 -1
  182. package/dist/sessionRecorder.d.ts +0 -54
  183. package/dist/sessionRecorder.js +0 -1
  184. package/dist/sessionRecorder.js.map +0 -1
  185. package/examples/sample-expo-app/README.md +0 -142
  186. package/examples/sample-expo-app/app/(tabs)/_layout.tsx +0 -60
  187. package/examples/sample-expo-app/app/(tabs)/explore.tsx +0 -110
  188. package/examples/sample-expo-app/app/(tabs)/index.tsx +0 -125
  189. package/examples/sample-expo-app/app/(tabs)/posts.tsx +0 -96
  190. package/examples/sample-expo-app/app/(tabs)/users.tsx +0 -131
  191. package/examples/sample-expo-app/app/+not-found.tsx +0 -32
  192. package/examples/sample-expo-app/app/_layout.tsx +0 -53
  193. package/examples/sample-expo-app/app/post/[id].tsx +0 -199
  194. package/examples/sample-expo-app/app/user/[id].tsx +0 -270
  195. package/examples/sample-expo-app/app.json +0 -42
  196. package/examples/sample-expo-app/assets/fonts/SpaceMono-Regular.ttf +0 -0
  197. package/examples/sample-expo-app/assets/images/adaptive-icon.png +0 -0
  198. package/examples/sample-expo-app/assets/images/favicon.png +0 -0
  199. package/examples/sample-expo-app/assets/images/icon.png +0 -0
  200. package/examples/sample-expo-app/assets/images/partial-react-logo.png +0 -0
  201. package/examples/sample-expo-app/assets/images/react-logo.png +0 -0
  202. package/examples/sample-expo-app/assets/images/react-logo@2x.png +0 -0
  203. package/examples/sample-expo-app/assets/images/react-logo@3x.png +0 -0
  204. package/examples/sample-expo-app/assets/images/splash-icon.png +0 -0
  205. package/examples/sample-expo-app/components/Collapsible.tsx +0 -45
  206. package/examples/sample-expo-app/components/ErrorView.tsx +0 -52
  207. package/examples/sample-expo-app/components/ExternalLink.tsx +0 -24
  208. package/examples/sample-expo-app/components/HapticTab.tsx +0 -18
  209. package/examples/sample-expo-app/components/HelloWave.tsx +0 -40
  210. package/examples/sample-expo-app/components/LoadingSpinner.tsx +0 -34
  211. package/examples/sample-expo-app/components/ParallaxScrollView.tsx +0 -82
  212. package/examples/sample-expo-app/components/ThemedText.tsx +0 -60
  213. package/examples/sample-expo-app/components/ThemedView.tsx +0 -14
  214. package/examples/sample-expo-app/components/ui/IconSymbol.ios.tsx +0 -32
  215. package/examples/sample-expo-app/components/ui/IconSymbol.tsx +0 -41
  216. package/examples/sample-expo-app/components/ui/TabBarBackground.ios.tsx +0 -19
  217. package/examples/sample-expo-app/components/ui/TabBarBackground.tsx +0 -6
  218. package/examples/sample-expo-app/constants/Colors.ts +0 -26
  219. package/examples/sample-expo-app/eslint.config.js +0 -10
  220. package/examples/sample-expo-app/hooks/useApi.ts +0 -41
  221. package/examples/sample-expo-app/hooks/useColorScheme.ts +0 -1
  222. package/examples/sample-expo-app/hooks/useColorScheme.web.ts +0 -21
  223. package/examples/sample-expo-app/hooks/useThemeColor.ts +0 -21
  224. package/examples/sample-expo-app/metro.config.js +0 -26
  225. package/examples/sample-expo-app/package-lock.json +0 -26296
  226. package/examples/sample-expo-app/package.json +0 -59
  227. package/examples/sample-expo-app/scripts/reset-project.js +0 -112
  228. package/examples/sample-expo-app/services/api.ts +0 -98
  229. package/examples/sample-expo-app/tsconfig.json +0 -17
  230. package/examples/sample-expo-app/utils/navigation.ts +0 -19
  231. package/src/otel/instrumentations/gestureInstrumentation.ts +0 -141
  232. package/src/otel/instrumentations/reactNativeInstrumentation.ts +0 -164
  233. package/src/otel/instrumentations/reactNavigationInstrumentation.ts +0 -114
  234. package/src/sessionRecorder.ts +0 -367
@@ -1,21 +1,275 @@
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'
1
13
 
2
- export function getExporterEndpoint(endpoint?: string): string {
3
- return endpoint || 'http://localhost:4318/v1/traces'
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
+ }
4
161
  }
5
162
 
6
- export function processHttpPayload(payload: string, maxSize: number = 1000000): string {
7
- if (payload.length <= maxSize) {
8
- return payload
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)
9
191
  }
10
192
 
11
- // Truncate payload if it's too large
12
- return payload.substring(0, maxSize) + '... [truncated]'
193
+ if (responseHeaders) {
194
+ span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, responseHeaders)
195
+ }
13
196
  }
14
197
 
15
- export function headersToObject(headers: Headers): Record<string, string> {
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> {
16
202
  const result: Record<string, string> = {}
17
- headers.forEach((value: string, key: string) => {
18
- result[key] = value
19
- })
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
+
20
227
  return result
21
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 CHANGED
@@ -8,95 +8,51 @@ import {
8
8
  ATTR_MULTIPLAYER_SESSION_ID,
9
9
  SessionRecorderIdGenerator,
10
10
  SessionRecorderTraceIdRatioBasedSampler,
11
+ SessionRecorderBrowserTraceExporter,
11
12
  } from '@multiplayer-app/session-recorder-common'
12
13
  import { TracerReactNativeConfig } from '../types'
13
14
  import { getInstrumentations } from './instrumentations'
14
15
  import { getExporterEndpoint } from './helpers'
15
- import { ReactNavigationInstrumentation } from './instrumentations/reactNavigationInstrumentation'
16
- import { GestureInstrumentation } from './instrumentations/gestureInstrumentation'
17
- import { getHttpMaskingConfig } from '../config/masking'
18
- import { getPlatformAttributes } from '../utils/platform'
19
-
20
16
 
21
- // Create a custom TracerProvider for React Native
22
- class ReactNativeTracerProvider {
23
- private resource: any
24
- private idGenerator: any
25
- private sampler: any
26
- private spanProcessors: any[]
27
- private propagator: any
17
+ import { getPlatformAttributes } from '../utils/platform'
18
+ import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
28
19
 
29
- constructor(options: any) {
30
- this.resource = options.resource
31
- this.idGenerator = options.idGenerator
32
- this.sampler = options.sampler
33
- this.spanProcessors = options.spanProcessors
34
- this.propagator = options.propagator
35
- }
36
20
 
37
- register(options: any) {
38
- // Register the provider with OpenTelemetry
39
- console.log('ReactNativeTracerProvider registered')
40
- }
41
21
 
42
- getTracer(name: string, version?: string, options?: any): any {
43
- // Return a mock tracer for now
44
- return {
45
- startSpan: (name: string, options?: any) => ({
46
- setAttribute: (key: string, value: any) => { },
47
- setStatus: (status: any) => { },
48
- end: () => { },
49
- recordException: (error: Error) => { },
50
- }),
51
- }
52
- }
53
- }
54
22
 
55
23
  export class TracerReactNativeSDK {
56
- private tracerProvider?: ReactNativeTracerProvider
24
+ private tracerProvider?: WebTracerProvider
57
25
  private config?: TracerReactNativeConfig
58
- private httpMaskingConfig: any
26
+
59
27
  private sessionId = ''
60
- private idGenerator: SessionRecorderIdGenerator
28
+ private idGenerator?: SessionRecorderIdGenerator
61
29
  private exporter?: any
62
- private navigationInstrumentation: ReactNavigationInstrumentation
63
- private gestureInstrumentation: GestureInstrumentation
64
30
  private isInitialized = false
65
31
 
66
- constructor() {
67
- this.idGenerator = new SessionRecorderIdGenerator()
68
- this.navigationInstrumentation = new ReactNavigationInstrumentation()
69
- this.gestureInstrumentation = new GestureInstrumentation()
70
- }
32
+ constructor() { }
71
33
 
72
34
  private _setSessionId(
73
35
  sessionId: string,
74
36
  sessionType: SessionType = SessionType.PLAIN,
75
37
  ) {
76
38
  this.sessionId = sessionId
77
- this.idGenerator.setSessionId(sessionId, sessionType)
39
+ this.idGenerator?.setSessionId(sessionId, sessionType)
78
40
  }
79
41
 
80
42
  init(options: TracerReactNativeConfig): void {
81
- if (this.isInitialized) {
82
- console.warn('TracerReactNativeSDK already initialized')
83
- return
84
- }
85
-
86
43
  this.config = options
87
- this.httpMaskingConfig = getHttpMaskingConfig(options.httpMasking)
44
+
88
45
  const { application, version, environment } = this.config
89
46
 
90
- // Create OTLP exporter
91
- const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http')
92
- this.exporter = new OTLPTraceExporter({
47
+ this.idGenerator = new SessionRecorderIdGenerator()
48
+
49
+ this.exporter = new SessionRecorderBrowserTraceExporter({
50
+ apiKey: options.apiKey,
93
51
  url: getExporterEndpoint(options.exporterEndpoint),
94
- headers: {
95
- 'Authorization': `Bearer ${options.apiKey}`,
96
- },
52
+ usePostMessageFallback: options.usePostMessageFallback,
97
53
  })
98
54
 
99
- this.tracerProvider = new ReactNativeTracerProvider({
55
+ this.tracerProvider = new WebTracerProvider({
100
56
  resource: resourceFromAttributes({
101
57
  [SemanticAttributes.SEMRESATTRS_SERVICE_NAME]: application,
102
58
  [SemanticAttributes.SEMRESATTRS_SERVICE_VERSION]: version,
@@ -122,7 +78,6 @@ export class TracerReactNativeSDK {
122
78
  })
123
79
 
124
80
  this.isInitialized = true
125
- console.log('TracerReactNativeSDK initialized successfully')
126
81
  }
127
82
 
128
83
  private _getSpanSessionIdProcessor() {
@@ -141,208 +96,43 @@ export class TracerReactNativeSDK {
141
96
  }
142
97
  }
143
98
 
144
- setSessionId(sessionId: string, sessionType: SessionType): void {
145
- this._setSessionId(sessionId, sessionType)
146
- }
147
-
148
- // Navigation instrumentation methods
149
- setNavigationRef(ref: any): void {
150
- this.navigationInstrumentation.setNavigationRef(ref)
151
- }
152
-
153
- recordNavigate(routeName: string, params?: Record<string, any>): void {
154
- this.navigationInstrumentation.recordNavigate(routeName, params)
155
- }
156
-
157
- recordGoBack(): void {
158
- this.navigationInstrumentation.recordGoBack()
159
- }
160
-
161
- recordReset(routes: any[]): void {
162
- this.navigationInstrumentation.recordReset(routes)
163
- }
164
-
165
- // Gesture instrumentation methods
166
- enableGestureTracking(): void {
167
- this.gestureInstrumentation.enable()
168
- }
169
-
170
- disableGestureTracking(): void {
171
- this.gestureInstrumentation.disable()
172
- }
173
-
174
- recordTap(x: number, y: number, target?: string): void {
175
- this.gestureInstrumentation.recordTap(x, y, target)
176
- }
177
-
178
- recordSwipe(direction: string, target?: string): void {
179
- this.gestureInstrumentation.recordSwipe(direction, target)
180
- }
181
-
182
- recordPinch(scale: number, target?: string): void {
183
- this.gestureInstrumentation.recordPinch(scale, target)
184
- }
185
-
186
- recordPan(deltaX: number, deltaY: number, target?: string): void {
187
- this.gestureInstrumentation.recordPan(deltaX, deltaY, target)
188
- }
189
-
190
- recordLongPress(duration: number, target?: string): void {
191
- this.gestureInstrumentation.recordLongPress(duration, target)
192
- }
193
-
194
- recordGestureError(error: Error, gestureType: string): void {
195
- this.gestureInstrumentation.recordGestureError(error, gestureType)
196
- }
197
-
198
- // Performance monitoring methods
199
- startTrace(name: string, attributes?: Record<string, any>): any {
200
- if (!this.isInitialized) {
201
- console.warn('TracerReactNativeSDK not initialized')
202
- return null
99
+ start(sessionId: string, sessionType: SessionType): void {
100
+ if (!this.tracerProvider) {
101
+ throw new Error(
102
+ 'Configuration not initialized. Call init() before start().',
103
+ )
203
104
  }
204
105
 
205
- const { trace } = require('@opentelemetry/api')
206
- const span = trace.startSpan(name, {
207
- attributes: {
208
- 'trace.type': 'performance',
209
- 'trace.name': name,
210
- ...attributes,
211
- },
212
- })
213
-
214
- return span
106
+ this._setSessionId(sessionId, sessionType)
215
107
  }
216
108
 
217
- endTrace(span: any, status?: any): void {
218
- if (span) {
219
- if (status) {
220
- span.setStatus(status)
221
- }
222
- span.end()
109
+ stop(): void {
110
+ if (!this.tracerProvider) {
111
+ throw new Error(
112
+ 'Configuration not initialized. Call init() before start().',
113
+ )
223
114
  }
224
- }
225
-
226
- // Error tracking
227
- captureException(error: Error, context?: Record<string, any>): void {
228
- if (!this.isInitialized) return
229
-
230
- const { trace, SpanStatusCode } = require('@opentelemetry/api')
231
- const span = trace.startSpan('error.capture', {
232
- attributes: {
233
- 'error.type': error.name,
234
- 'error.message': error.message,
235
- 'error.stack': error.stack,
236
- ...context,
237
- },
238
- })
239
-
240
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
241
- span.recordException(error)
242
- span.end()
243
- }
244
-
245
- // Network monitoring
246
- recordNetworkRequest(url: string, method: string, statusCode: number, duration: number): void {
247
- if (!this.isInitialized) return
248
-
249
- const { trace } = require('@opentelemetry/api')
250
- const span = trace.startSpan('network.request', {
251
- attributes: {
252
- 'http.url': url,
253
- 'http.method': method,
254
- 'http.status_code': statusCode,
255
- 'http.duration': duration,
256
- 'network.type': 'fetch',
257
- },
258
- })
259
-
260
- span.setStatus({ code: statusCode >= 400 ? 2 : 1 })
261
- span.end()
262
- }
263
-
264
- // Screen performance
265
- recordScreenLoad(screenName: string, loadTime: number): void {
266
- if (!this.isInitialized) return
267
-
268
- const { trace } = require('@opentelemetry/api')
269
- const span = trace.startSpan('screen.load', {
270
- attributes: {
271
- 'screen.name': screenName,
272
- 'screen.load_time': loadTime,
273
- 'screen.type': 'react_native',
274
- },
275
- })
276
115
 
277
- span.end()
116
+ this._setSessionId('')
278
117
  }
279
118
 
280
- // Memory monitoring
281
- recordMemoryUsage(usedMemory: number, totalMemory: number): void {
282
- if (!this.isInitialized) return
283
-
284
- const { trace } = require('@opentelemetry/api')
285
- const span = trace.startSpan('memory.usage', {
286
- attributes: {
287
- 'memory.used': usedMemory,
288
- 'memory.total': totalMemory,
289
- 'memory.percentage': (usedMemory / totalMemory) * 100,
290
- },
291
- })
292
-
293
- span.end()
294
- }
295
-
296
- // Battery monitoring
297
- recordBatteryLevel(level: number, isCharging: boolean): void {
298
- if (!this.isInitialized) return
299
-
300
- const { trace } = require('@opentelemetry/api')
301
- const span = trace.startSpan('battery.status', {
302
- attributes: {
303
- 'battery.level': level,
304
- 'battery.charging': isCharging,
305
- },
306
- })
307
-
308
- span.end()
309
- }
310
-
311
- // App lifecycle events
312
- recordAppStateChange(state: string): void {
313
- if (!this.isInitialized) return
314
-
315
- const { trace } = require('@opentelemetry/api')
316
- const span = trace.startSpan('app.state_change', {
317
- attributes: {
318
- 'app.state': state,
319
- 'app.timestamp': Date.now(),
320
- },
321
- })
119
+ setApiKey(apiKey: string): void {
120
+ if (!this.exporter) {
121
+ throw new Error(
122
+ 'Configuration not initialized. Call init() before setApiKey().',
123
+ )
124
+ }
322
125
 
323
- span.end()
126
+ this.exporter.setApiKey?.(apiKey)
324
127
  }
325
128
 
326
- // Custom event tracking
327
- recordCustomEvent(eventName: string, attributes?: Record<string, any>): void {
328
- if (!this.isInitialized) return
329
-
330
- const { trace } = require('@opentelemetry/api')
331
- const span = trace.startSpan(`custom.${eventName}`, {
332
- attributes: {
333
- 'event.name': eventName,
334
- 'event.type': 'custom',
335
- ...attributes,
336
- },
337
- })
338
-
339
- span.end()
129
+ setSessionId(sessionId: string, sessionType: SessionType): void {
130
+ this._setSessionId(sessionId, sessionType)
340
131
  }
341
132
 
342
- // Shutdown
133
+ // Shutdown (React Native specific)
343
134
  shutdown(): Promise<void> {
344
135
  this.isInitialized = false
345
- console.log('TracerReactNativeSDK shutdown')
346
136
  return Promise.resolve()
347
137
  }
348
138
  }