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

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 (166) hide show
  1. package/dist/config/constants.d.ts +19 -0
  2. package/dist/config/constants.js +1 -0
  3. package/dist/config/constants.js.map +1 -0
  4. package/dist/config/defaults.d.ts +4 -0
  5. package/dist/config/defaults.js +1 -0
  6. package/dist/config/defaults.js.map +1 -0
  7. package/dist/config/index.d.ts +5 -0
  8. package/dist/config/index.js +1 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/config/masking.d.ts +2 -30
  11. package/dist/config/masking.js +1 -1
  12. package/dist/config/masking.js.map +1 -1
  13. package/dist/config/session-recorder.d.ts +2 -0
  14. package/dist/config/session-recorder.js +1 -0
  15. package/dist/config/session-recorder.js.map +1 -0
  16. package/dist/config/validators.d.ts +10 -0
  17. package/dist/config/validators.js +1 -0
  18. package/dist/config/validators.js.map +1 -0
  19. package/dist/expo.d.ts +2 -2
  20. package/dist/expo.js +1 -1
  21. package/dist/expo.js.map +1 -1
  22. package/dist/exporters.d.ts +3 -0
  23. package/dist/exporters.js +1 -0
  24. package/dist/exporters.js.map +1 -0
  25. package/dist/index.d.ts +3 -9
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/otel/helpers.d.ts +45 -3
  29. package/dist/otel/helpers.js +1 -1
  30. package/dist/otel/helpers.js.map +1 -1
  31. package/dist/otel/index.d.ts +9 -5
  32. package/dist/otel/index.js +1 -1
  33. package/dist/otel/index.js.map +1 -1
  34. package/dist/otel/instrumentations/index.js +1 -1
  35. package/dist/otel/instrumentations/index.js.map +1 -1
  36. package/dist/patch/index.d.ts +1 -0
  37. package/dist/patch/index.js +1 -0
  38. package/dist/patch/index.js.map +1 -0
  39. package/dist/patch/xhr.d.ts +2 -0
  40. package/dist/patch/xhr.js +1 -0
  41. package/dist/patch/xhr.js.map +1 -0
  42. package/dist/recorder/gestureRecorder.js +1 -1
  43. package/dist/recorder/gestureRecorder.js.map +1 -1
  44. package/dist/recorder/navigationTracker.js +1 -1
  45. package/dist/recorder/navigationTracker.js.map +1 -1
  46. package/dist/recorder/screenRecorder.d.ts +1 -0
  47. package/dist/recorder/screenRecorder.js +1 -1
  48. package/dist/recorder/screenRecorder.js.map +1 -1
  49. package/dist/services/api.service.d.ts +62 -10
  50. package/dist/services/api.service.js +1 -1
  51. package/dist/services/api.service.js.map +1 -1
  52. package/dist/services/storage.service.d.ts +23 -16
  53. package/dist/services/storage.service.js +1 -1
  54. package/dist/services/storage.service.js.map +1 -1
  55. package/dist/session-recorder.d.ts +131 -0
  56. package/dist/session-recorder.js +1 -0
  57. package/dist/session-recorder.js.map +1 -0
  58. package/dist/sessionRecorder.d.ts +113 -34
  59. package/dist/sessionRecorder.js +1 -1
  60. package/dist/sessionRecorder.js.map +1 -1
  61. package/dist/types/index.d.ts +2 -81
  62. package/dist/types/index.js +1 -1
  63. package/dist/types/index.js.map +1 -1
  64. package/dist/types/session-recorder.d.ts +366 -0
  65. package/dist/types/session-recorder.js +1 -0
  66. package/dist/types/session-recorder.js.map +1 -0
  67. package/dist/types/session.d.ts +59 -0
  68. package/dist/types/session.js +1 -0
  69. package/dist/types/session.js.map +1 -0
  70. package/dist/utils/index.d.ts +5 -0
  71. package/dist/utils/index.js +1 -0
  72. package/dist/utils/index.js.map +1 -0
  73. package/dist/utils/platform.d.ts +5 -0
  74. package/dist/utils/platform.js +1 -1
  75. package/dist/utils/platform.js.map +1 -1
  76. package/dist/utils/request-utils.d.ts +21 -0
  77. package/dist/utils/request-utils.js +1 -0
  78. package/dist/utils/request-utils.js.map +1 -0
  79. package/dist/utils/session.d.ts +5 -0
  80. package/dist/utils/session.js +1 -0
  81. package/dist/utils/session.js.map +1 -0
  82. package/dist/utils/time.d.ts +4 -0
  83. package/dist/utils/time.js +1 -0
  84. package/dist/utils/time.js.map +1 -0
  85. package/dist/utils/type-utils.d.ts +16 -0
  86. package/dist/utils/type-utils.js +1 -0
  87. package/dist/utils/type-utils.js.map +1 -0
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +3 -3
  91. package/src/config/constants.ts +60 -0
  92. package/src/config/defaults.ts +82 -0
  93. package/src/config/index.ts +6 -0
  94. package/src/config/masking.ts +10 -61
  95. package/src/config/session-recorder.ts +55 -0
  96. package/src/config/validators.ts +31 -0
  97. package/src/expo.ts +2 -22
  98. package/src/index.ts +3 -15
  99. package/src/otel/helpers.ts +247 -11
  100. package/src/otel/index.ts +109 -76
  101. package/src/otel/instrumentations/index.ts +8 -8
  102. package/src/patch/index.ts +1 -0
  103. package/src/patch/xhr.ts +142 -0
  104. package/src/recorder/gestureRecorder.ts +12 -12
  105. package/src/recorder/navigationTracker.ts +10 -10
  106. package/src/recorder/screenRecorder.ts +26 -26
  107. package/src/services/api.service.ts +169 -37
  108. package/src/services/storage.service.ts +101 -74
  109. package/src/session-recorder.ts +570 -0
  110. package/src/types/index.ts +2 -88
  111. package/src/types/session-recorder.ts +423 -0
  112. package/src/types/session.ts +65 -0
  113. package/src/utils/index.ts +6 -0
  114. package/src/utils/platform.ts +13 -0
  115. package/src/utils/request-utils.ts +61 -0
  116. package/src/utils/session.ts +18 -0
  117. package/src/utils/time.ts +17 -0
  118. package/src/utils/type-utils.ts +75 -0
  119. package/src/version.ts +1 -1
  120. package/examples/sample-expo-app/README.md +0 -142
  121. package/examples/sample-expo-app/app/(tabs)/_layout.tsx +0 -60
  122. package/examples/sample-expo-app/app/(tabs)/explore.tsx +0 -110
  123. package/examples/sample-expo-app/app/(tabs)/index.tsx +0 -125
  124. package/examples/sample-expo-app/app/(tabs)/posts.tsx +0 -96
  125. package/examples/sample-expo-app/app/(tabs)/users.tsx +0 -131
  126. package/examples/sample-expo-app/app/+not-found.tsx +0 -32
  127. package/examples/sample-expo-app/app/_layout.tsx +0 -53
  128. package/examples/sample-expo-app/app/post/[id].tsx +0 -199
  129. package/examples/sample-expo-app/app/user/[id].tsx +0 -270
  130. package/examples/sample-expo-app/app.json +0 -42
  131. package/examples/sample-expo-app/assets/fonts/SpaceMono-Regular.ttf +0 -0
  132. package/examples/sample-expo-app/assets/images/adaptive-icon.png +0 -0
  133. package/examples/sample-expo-app/assets/images/favicon.png +0 -0
  134. package/examples/sample-expo-app/assets/images/icon.png +0 -0
  135. package/examples/sample-expo-app/assets/images/partial-react-logo.png +0 -0
  136. package/examples/sample-expo-app/assets/images/react-logo.png +0 -0
  137. package/examples/sample-expo-app/assets/images/react-logo@2x.png +0 -0
  138. package/examples/sample-expo-app/assets/images/react-logo@3x.png +0 -0
  139. package/examples/sample-expo-app/assets/images/splash-icon.png +0 -0
  140. package/examples/sample-expo-app/components/Collapsible.tsx +0 -45
  141. package/examples/sample-expo-app/components/ErrorView.tsx +0 -52
  142. package/examples/sample-expo-app/components/ExternalLink.tsx +0 -24
  143. package/examples/sample-expo-app/components/HapticTab.tsx +0 -18
  144. package/examples/sample-expo-app/components/HelloWave.tsx +0 -40
  145. package/examples/sample-expo-app/components/LoadingSpinner.tsx +0 -34
  146. package/examples/sample-expo-app/components/ParallaxScrollView.tsx +0 -82
  147. package/examples/sample-expo-app/components/ThemedText.tsx +0 -60
  148. package/examples/sample-expo-app/components/ThemedView.tsx +0 -14
  149. package/examples/sample-expo-app/components/ui/IconSymbol.ios.tsx +0 -32
  150. package/examples/sample-expo-app/components/ui/IconSymbol.tsx +0 -41
  151. package/examples/sample-expo-app/components/ui/TabBarBackground.ios.tsx +0 -19
  152. package/examples/sample-expo-app/components/ui/TabBarBackground.tsx +0 -6
  153. package/examples/sample-expo-app/constants/Colors.ts +0 -26
  154. package/examples/sample-expo-app/eslint.config.js +0 -10
  155. package/examples/sample-expo-app/hooks/useApi.ts +0 -41
  156. package/examples/sample-expo-app/hooks/useColorScheme.ts +0 -1
  157. package/examples/sample-expo-app/hooks/useColorScheme.web.ts +0 -21
  158. package/examples/sample-expo-app/hooks/useThemeColor.ts +0 -21
  159. package/examples/sample-expo-app/metro.config.js +0 -26
  160. package/examples/sample-expo-app/package-lock.json +0 -26296
  161. package/examples/sample-expo-app/package.json +0 -59
  162. package/examples/sample-expo-app/scripts/reset-project.js +0 -112
  163. package/examples/sample-expo-app/services/api.ts +0 -98
  164. package/examples/sample-expo-app/tsconfig.json +0 -17
  165. package/examples/sample-expo-app/utils/navigation.ts +0 -19
  166. package/src/sessionRecorder.ts +0 -367
@@ -0,0 +1,60 @@
1
+
2
+ export const OTEL_MP_SAMPLE_TRACE_RATIO = 0.15
3
+
4
+ export const SESSION_ID_PROP_NAME = 'multiplayer-session-id'
5
+
6
+ export const SESSION_SHORT_ID_PROP_NAME = 'multiplayer-session-short-id'
7
+
8
+ export const SESSION_CONTINUOUS_DEBUGGING_PROP_NAME = 'multiplayer-session-continuous-debugging'
9
+
10
+ export const SESSION_STATE_PROP_NAME = 'multiplayer-session-state'
11
+
12
+ export const SESSION_TYPE_PROP_NAME = 'multiplayer-session-type'
13
+
14
+ export const SESSION_PROP_NAME = 'multiplayer-session-data'
15
+
16
+ export const SESSION_STARTED_EVENT = 'debug-session:started'
17
+
18
+ export const SESSION_STOPPED_EVENT = 'debug-session:stopped'
19
+
20
+ export const SESSION_SUBSCRIBE_EVENT = 'debug-session:subscribe'
21
+
22
+ export const SESSION_UNSUBSCRIBE_EVENT = 'debug-session:unsubscribe'
23
+
24
+ export const SESSION_AUTO_CREATED = 'debug-session:auto-created'
25
+
26
+ export const SESSION_ADD_EVENT = 'debug-session:rrweb:add-event'
27
+
28
+ export const DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE = 100000
29
+
30
+ export const SESSION_RESPONSE = 'multiplayer-debug-session-response'
31
+
32
+ export const CONTINUOUS_DEBUGGING_TIMEOUT = 60000 // 1 minutes
33
+
34
+ export const DEBUG_SESSION_MAX_DURATION_SECONDS = 10 * 60 + 30 // TODO: move to shared config otel core
35
+
36
+ // Package version - injected by webpack during build
37
+ declare const PACKAGE_VERSION: string
38
+ export const PACKAGE_VERSION_EXPORT = PACKAGE_VERSION || '1.0.0'
39
+
40
+
41
+ // Regex patterns for OpenTelemetry ignore URLs
42
+ export const OTEL_IGNORE_URLS = [
43
+ // Traces endpoint
44
+ /.*\/v1\/traces/,
45
+ // Debug sessions endpoints
46
+ /.*\/v0\/radar\/debug-sessions\/start$/,
47
+ /.*\/v0\/radar\/debug-sessions\/[^/]+\/stop$/,
48
+ /.*\/v0\/radar\/debug-sessions\/[^/]+\/cancel$/,
49
+
50
+ // Continuous debug sessions endpoints
51
+ /.*\/v0\/radar\/continuous-debug-sessions\/start$/,
52
+ /.*\/v0\/radar\/continuous-debug-sessions\/[^/]+\/save$/,
53
+ /.*\/v0\/radar\/continuous-debug-sessions\/[^/]+\/cancel$/,
54
+
55
+ // Remote debug session endpoint
56
+ /.*\/v0\/radar\/remote-debug-session\/check$/,
57
+
58
+ // Or use a more general pattern to catch all radar API endpoints
59
+ // /.*\/v0\/radar\/.*/
60
+ ]
@@ -0,0 +1,82 @@
1
+ import {
2
+ SessionRecorderSdk,
3
+ MULTIPLAYER_BASE_API_URL,
4
+ MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL,
5
+ } from '@multiplayer-app/session-recorder-common'
6
+ import {
7
+ MaskingConfig,
8
+ SessionRecorderConfigs,
9
+ WidgetButtonPlacement,
10
+ WidgetTextOverridesConfig,
11
+ } from '../types'
12
+ import {
13
+ OTEL_MP_SAMPLE_TRACE_RATIO,
14
+ DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE,
15
+ } from './constants'
16
+ const { mask, sensitiveFields, sensitiveHeaders } = SessionRecorderSdk
17
+
18
+ export const DEFAULT_MASKING_CONFIG: MaskingConfig = {
19
+ isContentMaskingEnabled: true,
20
+ maskBody: mask(sensitiveFields),
21
+ maskHeaders: mask(sensitiveHeaders),
22
+ maskBodyFieldsList: sensitiveFields,
23
+ maskHeadersList: sensitiveHeaders,
24
+ headersToInclude: [],
25
+ headersToExclude: [],
26
+ }
27
+
28
+ export const DEFAULT_WIDGET_TEXT_CONFIG: WidgetTextOverridesConfig = {
29
+ initialTitleWithContinuous: 'Encountered an issue?',
30
+ initialTitleWithoutContinuous: 'Encountered an issue?',
31
+ initialDescriptionWithContinuous: 'Record your session so we can see the problem and fix it faster.',
32
+ initialDescriptionWithoutContinuous: 'Record your session so we can see the problem and fix it faster.',
33
+ continuousRecordingLabel: 'Continuous recording',
34
+ startRecordingButtonText: 'Start recording',
35
+ finalTitle: 'Done recording?',
36
+ finalDescription: 'You can also add a quick note with extra context, expectations, or questions. Thank you!',
37
+ commentPlaceholder: 'Add a message...',
38
+ saveButtonText: 'Submit recording',
39
+ cancelButtonText: 'Cancel recording',
40
+ continuousOverlayTitle: 'Save time, skip the reproductions',
41
+ continuousOverlayDescription: 'We keep a rolling record of your recent activity. If something doesn’t work as expected, just save the recording and continue working. No need to worry about exceptions and errors - we automatically save recordings for those!',
42
+ saveLastSnapshotButtonText: 'Save recording',
43
+ submitDialogTitle: 'Save recording',
44
+ submitDialogSubtitle: 'This full-stack session recording will be saved directly to your selected Multiplayer project. All data is automatically correlated end-to-end.',
45
+ submitDialogCommentLabel: 'You can also add context, comments, or notes.',
46
+ submitDialogCommentPlaceholder: 'Add a message...',
47
+ submitDialogSubmitText: 'Save',
48
+ submitDialogCancelText: 'Cancel',
49
+ }
50
+
51
+ export const BASE_CONFIG: Required<SessionRecorderConfigs> = {
52
+ apiKey: '',
53
+
54
+ version: '',
55
+ application: '',
56
+ environment: '',
57
+
58
+ showWidget: true,
59
+ showContinuousRecording: true,
60
+ widgetButtonPlacement: WidgetButtonPlacement.bottomRight,
61
+
62
+ usePostMessageFallback: false,
63
+ apiBaseUrl: MULTIPLAYER_BASE_API_URL,
64
+ exporterEndpoint: MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL,
65
+
66
+ schemifyDocSpanPayload: true,
67
+
68
+ ignoreUrls: [],
69
+ propagateTraceHeaderCorsUrls: [],
70
+
71
+ sampleTraceRatio: OTEL_MP_SAMPLE_TRACE_RATIO,
72
+ maxCapturingHttpPayloadSize: DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE,
73
+
74
+ captureBody: true,
75
+ captureHeaders: true,
76
+ masking: DEFAULT_MASKING_CONFIG,
77
+ widgetTextOverrides: DEFAULT_WIDGET_TEXT_CONFIG,
78
+
79
+ recordScreen: true,
80
+ recordGestures: true,
81
+ recordNavigation: true,
82
+ }
@@ -0,0 +1,6 @@
1
+ // Export all config-related functions and constants
2
+ export * from './constants'
3
+ export * from './defaults'
4
+ export * from './validators'
5
+ export * from './masking'
6
+ export * from './session-recorder'
@@ -1,63 +1,12 @@
1
+ import { MaskingConfig } from '../types'
2
+ import { DEFAULT_MASKING_CONFIG } from './defaults'
3
+ import { isValidArray, isValidBoolean, isValidFunction } from './validators'
1
4
  import { SessionRecorderSdk } from '@multiplayer-app/session-recorder-common'
2
5
 
3
6
  const { mask, sensitiveFields, sensitiveHeaders } = SessionRecorderSdk
4
7
 
5
- /**
6
- * HTTP Masking Configuration Interface
7
- * Focused only on HTTP request/response masking for OpenTelemetry traces
8
- */
9
- export interface HttpMaskingConfig {
10
- /** If true, enables masking for debug span payload in traces */
11
- isContentMaskingEnabled?: boolean
12
- /** Custom function for masking body in traces */
13
- maskBody?: (payload: any, span: any) => any
14
- /** Custom function for masking headers in traces */
15
- maskHeaders?: (headers: any, span: any) => any
16
- /** List of body fields to mask in traces */
17
- maskBodyFieldsList?: string[]
18
- /** List of headers to mask in traces */
19
- maskHeadersList?: string[]
20
- /** List of headers to include in traces (if specified, only these headers will be captured) */
21
- headersToInclude?: string[]
22
- /** List of headers to exclude from traces */
23
- headersToExclude?: string[]
24
- }
25
-
26
- /**
27
- * Default HTTP masking configuration
28
- */
29
- export const DEFAULT_HTTP_MASKING_CONFIG: HttpMaskingConfig = {
30
- isContentMaskingEnabled: true,
31
- maskBody: mask(sensitiveFields),
32
- maskHeaders: mask(sensitiveHeaders),
33
- maskBodyFieldsList: sensitiveFields,
34
- maskHeadersList: sensitiveHeaders,
35
- headersToInclude: [],
36
- headersToExclude: [],
37
- }
38
-
39
- /**
40
- * Validation helper functions
41
- */
42
- const isValidArray = (value: any[] | undefined, defaultValue: any[]): any[] => {
43
- return Array.isArray(value) ? value : defaultValue
44
- }
45
-
46
- const isValidBoolean = (value: boolean | undefined, defaultValue: boolean): boolean => {
47
- return typeof value === 'boolean' ? value : defaultValue
48
- }
49
-
50
- const isValidFunction = (value: any, defaultValue: any): any => {
51
- return typeof value === 'function' ? value : defaultValue
52
- }
53
-
54
- /**
55
- * Get HTTP masking configuration
56
- * @param masking Optional masking configuration
57
- * @returns Processed HTTP masking configuration
58
- */
59
- export const getHttpMaskingConfig = (masking?: HttpMaskingConfig): HttpMaskingConfig => {
60
- const baseMasking = DEFAULT_HTTP_MASKING_CONFIG
8
+ export const getMaskingConfig = (masking?: MaskingConfig): MaskingConfig => {
9
+ const baseMasking = DEFAULT_MASKING_CONFIG
61
10
 
62
11
  if (typeof masking !== 'object') {
63
12
  return baseMasking
@@ -67,12 +16,12 @@ export const getHttpMaskingConfig = (masking?: HttpMaskingConfig): HttpMaskingCo
67
16
  const maskBodyFieldsList = isValidArray(masking.maskBodyFieldsList, sensitiveFields)
68
17
 
69
18
  return {
70
- isContentMaskingEnabled: isValidBoolean(masking.isContentMaskingEnabled, baseMasking.isContentMaskingEnabled ?? true),
71
- maskBody: isValidFunction(masking.maskBody, mask(maskBodyFieldsList)),
72
- maskHeaders: isValidFunction(masking.maskHeaders, mask(maskHeadersList)),
73
- maskBodyFieldsList,
74
19
  maskHeadersList,
20
+ maskBodyFieldsList,
75
21
  headersToInclude: isValidArray(masking.headersToInclude, baseMasking.headersToInclude ?? []),
76
22
  headersToExclude: isValidArray(masking.headersToExclude, baseMasking.headersToExclude ?? []),
23
+ isContentMaskingEnabled: isValidBoolean(masking.isContentMaskingEnabled, baseMasking.isContentMaskingEnabled ?? true),
24
+ maskBody: isValidFunction(masking.maskBody, mask(maskBodyFieldsList)),
25
+ maskHeaders: isValidFunction(masking.maskHeaders, mask(maskHeadersList)),
77
26
  }
78
- }
27
+ }
@@ -0,0 +1,55 @@
1
+ import { SessionRecorderConfigs, SessionRecorderOptions, WidgetButtonPlacement } from '../types'
2
+ import { BASE_CONFIG } from './defaults'
3
+ import { getMaskingConfig } from './masking'
4
+ import {
5
+ isValidString,
6
+ isValidNumber,
7
+ isValidBoolean,
8
+ isValidArray,
9
+ isValidEnum,
10
+ } from './validators'
11
+
12
+ const getWidgetTextOverridesConfig = (config: any, defaultConfig: any) => {
13
+ if (!config || typeof config !== 'object') {
14
+ return defaultConfig
15
+ }
16
+ return Object.keys(defaultConfig).reduce((acc, key) => {
17
+ acc[key] = isValidString(config[key], defaultConfig[key])
18
+ return acc
19
+ }, {} as Record<string, any>)
20
+ }
21
+
22
+ export const getSessionRecorderConfig = (c: SessionRecorderOptions): SessionRecorderConfigs => {
23
+ if (!c) {
24
+ return BASE_CONFIG
25
+ }
26
+
27
+ return {
28
+ apiKey: isValidString(c.apiKey, BASE_CONFIG.apiKey),
29
+ version: isValidString(c.version, BASE_CONFIG.version),
30
+ application: isValidString(c.application, BASE_CONFIG.application),
31
+ environment: isValidString(c.environment, BASE_CONFIG.environment),
32
+
33
+ exporterEndpoint: isValidString(c.exporterEndpoint, BASE_CONFIG.exporterEndpoint),
34
+ apiBaseUrl: isValidString(c.apiBaseUrl, BASE_CONFIG.apiBaseUrl),
35
+ usePostMessageFallback: isValidBoolean(c.usePostMessageFallback, BASE_CONFIG.usePostMessageFallback),
36
+
37
+ showWidget: isValidBoolean(c.showWidget, BASE_CONFIG.showWidget),
38
+ showContinuousRecording: isValidBoolean(c.showContinuousRecording, BASE_CONFIG.showContinuousRecording),
39
+ widgetButtonPlacement: isValidEnum<WidgetButtonPlacement>(c.widgetButtonPlacement, BASE_CONFIG.widgetButtonPlacement, Object.values(WidgetButtonPlacement) as WidgetButtonPlacement[]),
40
+ ignoreUrls: isValidArray(c.ignoreUrls, BASE_CONFIG.ignoreUrls),
41
+ sampleTraceRatio: isValidNumber(c.sampleTraceRatio, BASE_CONFIG.sampleTraceRatio),
42
+ propagateTraceHeaderCorsUrls: c.propagateTraceHeaderCorsUrls || BASE_CONFIG.propagateTraceHeaderCorsUrls,
43
+ schemifyDocSpanPayload: isValidBoolean(c.schemifyDocSpanPayload, BASE_CONFIG.schemifyDocSpanPayload),
44
+ maxCapturingHttpPayloadSize: isValidNumber(c.maxCapturingHttpPayloadSize, BASE_CONFIG.maxCapturingHttpPayloadSize),
45
+
46
+ masking: getMaskingConfig(c.masking),
47
+ captureBody: isValidBoolean(c.captureBody, BASE_CONFIG.captureBody),
48
+ captureHeaders: isValidBoolean(c.captureHeaders, BASE_CONFIG.captureHeaders),
49
+ widgetTextOverrides: getWidgetTextOverridesConfig(c.widgetTextOverrides, BASE_CONFIG.widgetTextOverrides),
50
+
51
+ recordScreen: isValidBoolean(c.recordScreen, BASE_CONFIG.recordScreen),
52
+ recordGestures: isValidBoolean(c.recordGestures, BASE_CONFIG.recordGestures),
53
+ recordNavigation: isValidBoolean(c.recordNavigation, BASE_CONFIG.recordNavigation),
54
+ }
55
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Validation helper functions for configuration objects
3
+ */
4
+
5
+ export const isValidStringOrRegExp = (value: string | RegExp | undefined, defaultValue: string | RegExp) => {
6
+ return typeof value === 'string' || value instanceof RegExp ? value : defaultValue
7
+ }
8
+
9
+ export const isValidString = <T extends string>(value: string | undefined | T, defaultValue: string) => {
10
+ return typeof value === 'string' ? value.trim() : defaultValue
11
+ }
12
+
13
+ export const isValidNumber = (value: number | undefined, defaultValue: number) => {
14
+ return typeof value === 'number' ? value : defaultValue
15
+ }
16
+
17
+ export const isValidBoolean = (value: boolean | undefined, defaultValue: boolean) => {
18
+ return typeof value === 'boolean' ? value : defaultValue
19
+ }
20
+
21
+ export const isValidArray = (value: any[] | undefined, defaultValue: any[]) => {
22
+ return Array.isArray(value) ? value : defaultValue
23
+ }
24
+
25
+ export const isValidEnum = <T>(value: any | T, defaultValue: T, enumValues: T[]): T => {
26
+ return enumValues.includes(value as T) ? value as T : defaultValue
27
+ }
28
+
29
+ export const isValidFunction = (value: any, defaultValue: any) => {
30
+ return typeof value === 'function' ? value : defaultValue
31
+ }
package/src/expo.ts CHANGED
@@ -1,34 +1,14 @@
1
- import { SessionRecorder } from './sessionRecorder'
2
- import { isExpoEnvironment } from './utils/platform'
1
+ import { SessionRecorder } from './session-recorder'
3
2
 
4
3
  // Create a specialized instance for Expo
5
4
  const SessionRecorderExpoInstance = new SessionRecorder()
6
5
 
7
- // Add Expo-specific initialization
8
- const originalInit = SessionRecorderExpoInstance.init.bind(SessionRecorderExpoInstance)
9
-
10
- SessionRecorderExpoInstance.init = async function(options: any) {
11
- // Add Expo-specific configuration
12
- const expoOptions = {
13
- ...options,
14
- // Add Expo-specific defaults
15
- platform: 'expo',
16
- }
17
-
18
- // Log Expo environment detection
19
- if (isExpoEnvironment()) {
20
- console.log('Multiplayer Session Recorder: Running in Expo environment')
21
- }
22
-
23
- return originalInit(expoOptions)
24
- }
25
-
26
6
  // Export the Expo instance as default
27
7
  export default SessionRecorderExpoInstance
28
8
 
29
9
  // Export types and classes
30
10
  export * from './types'
31
- export { SessionRecorder } from './sessionRecorder'
11
+ export { SessionRecorder } from './session-recorder'
32
12
  export { TracerReactNativeSDK } from './otel'
33
13
  export { RecorderReactNativeSDK } from './recorder'
34
14
  export { ApiService } from './services/api.service'
package/src/index.ts CHANGED
@@ -1,20 +1,8 @@
1
- import { SessionRecorder } from './sessionRecorder'
1
+ import './patch'
2
+ import { SessionRecorder } from './session-recorder'
3
+ export * from '@multiplayer-app/session-recorder-common'
2
4
 
3
5
  const SessionRecorderInstance = new SessionRecorder()
4
6
 
5
7
  // Export the instance as default
6
8
  export default SessionRecorderInstance
7
-
8
- // Export types and classes
9
- export * from './types'
10
- export { SessionRecorder } from './sessionRecorder'
11
- export { TracerReactNativeSDK } from './otel'
12
- export { RecorderReactNativeSDK } from './recorder'
13
- export { ApiService } from './services/api.service'
14
- export { StorageService } from './services/storage.service'
15
-
16
- // Export common types
17
- export * from '@multiplayer-app/session-recorder-common'
18
-
19
- // Export platform utilities
20
- export { isExpoEnvironment, getPlatformAttributes, detectPlatform } from './utils/platform'
@@ -1,21 +1,257 @@
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 { SessionRecorderSdk } from '@multiplayer-app/session-recorder-common'
11
+ import { TracerReactNativeConfig } from '../types'
1
12
 
2
- export function getExporterEndpoint(endpoint?: string): string {
3
- return endpoint || 'http://localhost:4318/v1/traces'
13
+ const { schemify } = SessionRecorderSdk
14
+
15
+ export interface HttpPayloadData {
16
+ requestBody?: any
17
+ responseBody?: any
18
+ requestHeaders?: Record<string, string>
19
+ responseHeaders?: Record<string, string>
20
+ }
21
+
22
+ export interface ProcessedHttpPayload {
23
+ requestBody?: string
24
+ responseBody?: string
25
+ requestHeaders?: string
26
+ responseHeaders?: string
27
+ }
28
+
29
+ /**
30
+ * Checks if the trace should be processed based on trace ID prefixes
31
+ */
32
+ export function shouldProcessTrace(traceId: string): boolean {
33
+ return (
34
+ traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
35
+ traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Processes request and response body based on trace type and configuration
41
+ */
42
+ export function processBody(
43
+ payload: HttpPayloadData,
44
+ config: TracerReactNativeConfig,
45
+ span: Span,
46
+ ): { requestBody?: string; responseBody?: string } {
47
+ const { captureBody, masking } = config
48
+ const traceId = span.spanContext().traceId
49
+
50
+ if (!captureBody) {
51
+ return {}
52
+ }
53
+
54
+ let { requestBody, responseBody } = payload
55
+
56
+ if (requestBody !== undefined && requestBody !== null) {
57
+ requestBody = JSON.parse(JSON.stringify(requestBody))
58
+ }
59
+ if (responseBody !== undefined && responseBody !== null) {
60
+ responseBody = JSON.parse(JSON.stringify(responseBody))
61
+ }
62
+
63
+ // Apply masking for debug traces
64
+ if (
65
+ traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
66
+ traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
67
+ ) {
68
+ if (masking?.isContentMaskingEnabled) {
69
+ requestBody = requestBody && masking.maskBody?.(requestBody, span)
70
+ responseBody = responseBody && masking.maskBody?.(responseBody, span)
71
+ }
72
+ }
73
+
74
+ // Convert to string if needed
75
+ if (typeof requestBody !== 'string') {
76
+ requestBody = JSON.stringify(requestBody)
77
+ }
78
+
79
+ if (typeof responseBody !== 'string') {
80
+ responseBody = JSON.stringify(responseBody)
81
+ }
82
+
83
+ return {
84
+ requestBody: requestBody?.length ? requestBody : undefined,
85
+ responseBody: responseBody?.length ? responseBody : undefined,
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Processes request and response headers based on configuration
91
+ */
92
+ export function processHeaders(
93
+ payload: HttpPayloadData,
94
+ config: TracerReactNativeConfig,
95
+ span: Span,
96
+ ): { requestHeaders?: string; responseHeaders?: string } {
97
+ const { captureHeaders, masking } = config
98
+
99
+ if (!captureHeaders) {
100
+ return {}
101
+ }
102
+
103
+ let { requestHeaders = {}, responseHeaders = {} } = payload
104
+
105
+ // Handle header filtering
106
+ if (
107
+ !masking?.headersToInclude?.length &&
108
+ !masking?.headersToExclude?.length
109
+ ) {
110
+ // Add null checks to prevent JSON.parse error when headers is undefined
111
+ if (requestHeaders !== undefined && requestHeaders !== null) {
112
+ requestHeaders = JSON.parse(JSON.stringify(requestHeaders))
113
+ }
114
+ if (responseHeaders !== undefined && responseHeaders !== null) {
115
+ responseHeaders = JSON.parse(JSON.stringify(responseHeaders))
116
+ }
117
+ } else {
118
+ if (masking?.headersToInclude) {
119
+ const _requestHeaders: Record<string, string> = {}
120
+ const _responseHeaders: Record<string, string> = {}
121
+
122
+ for (const headerName of masking.headersToInclude) {
123
+ if (requestHeaders[headerName]) {
124
+ _requestHeaders[headerName] = requestHeaders[headerName]
125
+ }
126
+ if (responseHeaders[headerName]) {
127
+ _responseHeaders[headerName] = responseHeaders[headerName]
128
+ }
129
+ }
130
+
131
+ requestHeaders = _requestHeaders
132
+ responseHeaders = _responseHeaders
133
+ }
134
+
135
+ if (masking?.headersToExclude?.length) {
136
+ for (const headerName of masking.headersToExclude) {
137
+ delete requestHeaders[headerName]
138
+ delete responseHeaders[headerName]
139
+ }
140
+ }
141
+ }
142
+
143
+ // Apply masking
144
+ const maskedRequestHeaders = masking?.maskHeaders?.(requestHeaders, span) || requestHeaders
145
+ const maskedResponseHeaders = masking?.maskHeaders?.(responseHeaders, span) || responseHeaders
146
+
147
+ // Convert to string
148
+ const requestHeadersStr = typeof maskedRequestHeaders === 'string'
149
+ ? maskedRequestHeaders
150
+ : JSON.stringify(maskedRequestHeaders)
151
+
152
+ const responseHeadersStr = typeof maskedResponseHeaders === 'string'
153
+ ? maskedResponseHeaders
154
+ : JSON.stringify(maskedResponseHeaders)
155
+
156
+ return {
157
+ requestHeaders: requestHeadersStr?.length ? requestHeadersStr : undefined,
158
+ responseHeaders: responseHeadersStr?.length ? responseHeadersStr : undefined,
159
+ }
4
160
  }
5
161
 
6
- export function processHttpPayload(payload: string, maxSize: number = 1000000): string {
7
- if (payload.length <= maxSize) {
8
- return payload
162
+ /**
163
+ * Processes HTTP payload (body and headers) and sets span attributes
164
+ */
165
+ export function processHttpPayload(
166
+ payload: HttpPayloadData,
167
+ config: TracerReactNativeConfig,
168
+ span: Span,
169
+ ): void {
170
+ const traceId = span.spanContext().traceId
171
+
172
+ if (!shouldProcessTrace(traceId)) {
173
+ return
9
174
  }
10
175
 
11
- // Truncate payload if it's too large
12
- return payload.substring(0, maxSize) + '... [truncated]'
176
+ const { requestBody, responseBody } = processBody(payload, config, span)
177
+ const { requestHeaders, responseHeaders } = processHeaders(payload, config, span)
178
+
179
+ // Set span attributes
180
+ if (requestBody) {
181
+ span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_BODY, requestBody)
182
+ }
183
+
184
+ if (responseBody) {
185
+ span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY, responseBody)
186
+ }
187
+
188
+ if (requestHeaders) {
189
+ span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS, requestHeaders)
190
+ }
191
+
192
+ if (responseHeaders) {
193
+ span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, responseHeaders)
194
+ }
13
195
  }
14
196
 
15
- export function headersToObject(headers: Headers): Record<string, string> {
197
+ /**
198
+ * Converts Headers object to plain object (React Native compatible)
199
+ */
200
+ export function headersToObject(headers: Record<string, string> | undefined): Record<string, string> {
16
201
  const result: Record<string, string> = {}
17
- headers.forEach((value: string, key: string) => {
18
- result[key] = value
19
- })
202
+
203
+ if (!headers) {
204
+ return result
205
+ }
206
+
207
+ if (typeof headers === 'object' && !Array.isArray(headers)) {
208
+ for (const [key, value] of Object.entries(headers)) {
209
+ if (typeof key === 'string' && typeof value === 'string') {
210
+ result[key] = value
211
+ }
212
+ }
213
+ }
214
+
20
215
  return result
21
216
  }
217
+
218
+ /**
219
+ * Extracts response body as string (React Native compatible)
220
+ */
221
+ export async function extractResponseBody(response: any): Promise<string | null> {
222
+ if (!response || !response.body) {
223
+ return null
224
+ }
225
+
226
+ try {
227
+ if (typeof response.body === 'string') {
228
+ return response.body
229
+ } else if (typeof response.body === 'object') {
230
+ return JSON.stringify(response.body)
231
+ } else {
232
+ return String(response.body)
233
+ }
234
+ } catch (error) {
235
+ // Failed to extract response body - silently continue
236
+ return null
237
+ }
238
+ }
239
+
240
+ export const getExporterEndpoint = (exporterEndpoint: string): string => {
241
+ const hasPath = exporterEndpoint && (() => {
242
+ try {
243
+ const url = new URL(exporterEndpoint)
244
+ return url.pathname !== '/' && url.pathname !== ''
245
+ } catch {
246
+ return false
247
+ }
248
+ })()
249
+
250
+ if (hasPath) {
251
+ return exporterEndpoint
252
+ }
253
+
254
+ const trimmedExporterEndpoint = new URL(exporterEndpoint).origin
255
+
256
+ return `${trimmedExporterEndpoint}/v1/traces`
257
+ }