@multiplayer-app/session-recorder-react-native 0.0.1-beta.6 → 0.0.1-beta.8

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 (76) hide show
  1. package/copy-react-native-dist.sh +3 -3
  2. package/docs/NATIVE_MODULE_SETUP.md +175 -0
  3. package/ios/SessionRecorderNative.podspec +5 -0
  4. package/package.json +11 -1
  5. package/plugin/package.json +20 -0
  6. package/plugin/src/index.js +42 -0
  7. package/react-native.config.js +1 -1
  8. package/android/src/main/AndroidManifest.xml +0 -2
  9. package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +0 -202
  10. package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +0 -16
  11. package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +0 -202
  12. package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +0 -16
  13. package/babel.config.js +0 -13
  14. package/docs/AUTO_METADATA_DETECTION.md +0 -108
  15. package/docs/TROUBLESHOOTING.md +0 -168
  16. package/ios/ScreenMasking.m +0 -12
  17. package/ios/ScreenMasking.podspec +0 -21
  18. package/ios/ScreenMasking.swift +0 -205
  19. package/ios/SessionRecorder.podspec +0 -21
  20. package/scripts/generate-app-metadata.js +0 -173
  21. package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +0 -86
  22. package/src/components/GestureCaptureWrapper/index.ts +0 -1
  23. package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +0 -72
  24. package/src/components/ScreenRecorderView/index.ts +0 -1
  25. package/src/components/SessionRecorderWidget/FinalPopover.tsx +0 -62
  26. package/src/components/SessionRecorderWidget/FloatingButton.tsx +0 -136
  27. package/src/components/SessionRecorderWidget/InitialPopover.tsx +0 -89
  28. package/src/components/SessionRecorderWidget/ModalContainer.tsx +0 -128
  29. package/src/components/SessionRecorderWidget/ModalHeader.tsx +0 -24
  30. package/src/components/SessionRecorderWidget/SessionRecorderWidget.tsx +0 -109
  31. package/src/components/SessionRecorderWidget/icons.tsx +0 -52
  32. package/src/components/SessionRecorderWidget/index.ts +0 -3
  33. package/src/components/SessionRecorderWidget/styles.ts +0 -150
  34. package/src/components/index.ts +0 -3
  35. package/src/config/constants.ts +0 -60
  36. package/src/config/defaults.ts +0 -83
  37. package/src/config/index.ts +0 -6
  38. package/src/config/masking.ts +0 -28
  39. package/src/config/session-recorder.ts +0 -55
  40. package/src/config/validators.ts +0 -31
  41. package/src/context/SessionRecorderContext.tsx +0 -53
  42. package/src/index.ts +0 -9
  43. package/src/native/ScreenMasking.ts +0 -34
  44. package/src/native/SessionRecorderNative.ts +0 -34
  45. package/src/otel/helpers.ts +0 -275
  46. package/src/otel/index.ts +0 -138
  47. package/src/otel/instrumentations/index.ts +0 -115
  48. package/src/patch/index.ts +0 -1
  49. package/src/patch/xhr.ts +0 -141
  50. package/src/recorder/eventExporter.ts +0 -141
  51. package/src/recorder/gestureRecorder.ts +0 -498
  52. package/src/recorder/index.ts +0 -179
  53. package/src/recorder/navigationTracker.ts +0 -449
  54. package/src/recorder/screenRecorder.ts +0 -527
  55. package/src/services/api.service.ts +0 -203
  56. package/src/services/screenMaskingService.ts +0 -118
  57. package/src/services/storage.service.ts +0 -199
  58. package/src/session-recorder.ts +0 -606
  59. package/src/types/expo.d.ts +0 -23
  60. package/src/types/index.ts +0 -28
  61. package/src/types/session-recorder.ts +0 -429
  62. package/src/types/session.ts +0 -65
  63. package/src/utils/app-metadata.ts +0 -31
  64. package/src/utils/index.ts +0 -8
  65. package/src/utils/logger.ts +0 -225
  66. package/src/utils/nativeModuleTest.ts +0 -60
  67. package/src/utils/platform.ts +0 -384
  68. package/src/utils/request-utils.ts +0 -61
  69. package/src/utils/rrweb-events.ts +0 -309
  70. package/src/utils/session.ts +0 -18
  71. package/src/utils/time.ts +0 -17
  72. package/src/utils/type-utils.ts +0 -75
  73. package/src/version.ts +0 -1
  74. package/tsconfig.json +0 -24
  75. /package/ios/{SessionRecorder.m → SessionRecorderNative.m} +0 -0
  76. /package/ios/{SessionRecorder.swift → SessionRecorderNative.swift} +0 -0
@@ -1,3 +0,0 @@
1
- export * from './GestureCaptureWrapper'
2
- export * from './ScreenRecorderView'
3
- export * from './SessionRecorderWidget'
@@ -1,60 +0,0 @@
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
- ]
@@ -1,83 +0,0 @@
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
- inputMasking: true,
27
- }
28
-
29
- export const DEFAULT_WIDGET_TEXT_CONFIG: WidgetTextOverridesConfig = {
30
- initialTitleWithContinuous: 'Encountered an issue?',
31
- initialTitleWithoutContinuous: 'Encountered an issue?',
32
- initialDescriptionWithContinuous: 'Record your session so we can see the problem and fix it faster.',
33
- initialDescriptionWithoutContinuous: 'Record your session so we can see the problem and fix it faster.',
34
- continuousRecordingLabel: 'Continuous recording',
35
- startRecordingButtonText: 'Start recording',
36
- finalTitle: 'Done recording?',
37
- finalDescription: 'You can also add a quick note with extra context, expectations, or questions. Thank you!',
38
- commentPlaceholder: 'Add a message...',
39
- saveButtonText: 'Submit recording',
40
- cancelButtonText: 'Cancel recording',
41
- continuousOverlayTitle: 'Save time, skip the reproductions',
42
- 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!',
43
- saveLastSnapshotButtonText: 'Save recording',
44
- submitDialogTitle: 'Save recording',
45
- submitDialogSubtitle: 'This full-stack session recording will be saved directly to your selected Multiplayer project. All data is automatically correlated end-to-end.',
46
- submitDialogCommentLabel: 'You can also add context, comments, or notes.',
47
- submitDialogCommentPlaceholder: 'Add a message...',
48
- submitDialogSubmitText: 'Save',
49
- submitDialogCancelText: 'Cancel',
50
- }
51
-
52
- export const BASE_CONFIG: Required<SessionRecorderConfigs> = {
53
- apiKey: '',
54
-
55
- version: '',
56
- application: '',
57
- environment: '',
58
-
59
- showWidget: true,
60
- showContinuousRecording: true,
61
- widgetButtonPlacement: WidgetButtonPlacement.bottomRight,
62
-
63
- usePostMessageFallback: false,
64
- apiBaseUrl: MULTIPLAYER_BASE_API_URL,
65
- exporterEndpoint: MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL,
66
-
67
- schemifyDocSpanPayload: true,
68
-
69
- ignoreUrls: [],
70
- propagateTraceHeaderCorsUrls: [],
71
-
72
- sampleTraceRatio: OTEL_MP_SAMPLE_TRACE_RATIO,
73
- maxCapturingHttpPayloadSize: DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE,
74
-
75
- captureBody: true,
76
- captureHeaders: true,
77
- masking: DEFAULT_MASKING_CONFIG,
78
- widgetTextOverrides: DEFAULT_WIDGET_TEXT_CONFIG,
79
-
80
- recordScreen: true,
81
- recordGestures: true,
82
- recordNavigation: true,
83
- }
@@ -1,6 +0,0 @@
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,28 +0,0 @@
1
- import { MaskingConfig } from '../types'
2
- import { DEFAULT_MASKING_CONFIG } from './defaults'
3
- import { isValidArray, isValidBoolean, isValidFunction } from './validators'
4
- import { SessionRecorderSdk } from '@multiplayer-app/session-recorder-common'
5
-
6
- const { mask, sensitiveFields, sensitiveHeaders } = SessionRecorderSdk
7
-
8
- export const getMaskingConfig = (masking?: MaskingConfig): MaskingConfig => {
9
- const baseMasking = DEFAULT_MASKING_CONFIG
10
-
11
- if (typeof masking !== 'object') {
12
- return baseMasking
13
- }
14
-
15
- const maskHeadersList = isValidArray(masking.maskHeadersList, sensitiveHeaders)
16
- const maskBodyFieldsList = isValidArray(masking.maskBodyFieldsList, sensitiveFields)
17
-
18
- return {
19
- maskHeadersList,
20
- maskBodyFieldsList,
21
- headersToInclude: isValidArray(masking.headersToInclude, baseMasking.headersToInclude ?? []),
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)),
26
- inputMasking: isValidBoolean(masking.inputMasking, baseMasking.inputMasking ?? true),
27
- }
28
- }
@@ -1,55 +0,0 @@
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
- }
@@ -1,31 +0,0 @@
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
- }
@@ -1,53 +0,0 @@
1
- import React, { createContext, useContext, PropsWithChildren, useState, useEffect, useRef } from 'react'
2
- import { SessionRecorderOptions, SessionState } from '../types'
3
- import sessionRecorder from '../session-recorder'
4
- import { ScreenRecorderView } from '../components/ScreenRecorderView'
5
- import SessionRecorderWidget from '../components/SessionRecorderWidget'
6
-
7
- interface SessionRecorderContextType {
8
- instance: typeof sessionRecorder
9
- isInitialized: boolean
10
- sessionState: SessionState | null
11
- }
12
-
13
- const SessionRecorderContext = createContext<SessionRecorderContextType | null>(null)
14
-
15
- export interface SessionRecorderProviderProps extends PropsWithChildren {
16
- options: SessionRecorderOptions
17
- }
18
-
19
- export const SessionRecorderProvider: React.FC<SessionRecorderProviderProps> = ({ children, options }) => {
20
- const [isInitialized, setIsInitialized] = useState(false)
21
- const [sessionState, setSessionState] = useState<SessionState | null>(SessionState.stopped)
22
- const optionsRef = useRef<string>()
23
-
24
- useEffect(() => {
25
- const newOptions = JSON.stringify(options)
26
- if (optionsRef.current === JSON.stringify(options)) return
27
- optionsRef.current = newOptions
28
- sessionRecorder.init(options)
29
- setIsInitialized(true)
30
- }, [options])
31
-
32
- useEffect(() => {
33
- setSessionState(sessionRecorder.sessionState)
34
- sessionRecorder.on('state-change', (state: SessionState) => {
35
- setSessionState(state)
36
- })
37
- }, [])
38
-
39
- return (
40
- <SessionRecorderContext.Provider value={{ instance: sessionRecorder, sessionState, isInitialized }}>
41
- <ScreenRecorderView>{children}</ScreenRecorderView>
42
- {isInitialized && !!sessionRecorder.config.showWidget && <SessionRecorderWidget />}
43
- </SessionRecorderContext.Provider>
44
- )
45
- }
46
-
47
- export const useSessionRecorder = (): SessionRecorderContextType => {
48
- const context = useContext(SessionRecorderContext)
49
- if (!context) {
50
- throw new Error('useSessionRecorder must be used within a SessionRecorderProvider')
51
- }
52
- return context
53
- }
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- import './patch'
2
- import SessionRecorder from './session-recorder'
3
- export * from '@multiplayer-app/session-recorder-common'
4
- export * from './context/SessionRecorderContext'
5
-
6
- // Export the class for type checking
7
- export { SessionRecorder }
8
- // Export the instance as default
9
- export default SessionRecorder
@@ -1,34 +0,0 @@
1
- import { NativeModules } from 'react-native'
2
-
3
- export interface MaskingOptions {
4
- /** Quality of the captured image (0.1 to 1.0) */
5
- quality?: number
6
- /** Whether to mask all input fields automatically */
7
- inputMasking?: boolean
8
- }
9
-
10
-
11
- export interface SessionRecorderNativeModule {
12
- /**
13
- * Capture the current screen and apply masking to sensitive elements
14
- * @returns Promise that resolves to base64 encoded image
15
- */
16
- captureAndMask(): Promise<string>
17
-
18
- /**
19
- * Capture the current screen and apply masking with custom options
20
- * @param options Custom masking options
21
- * @returns Promise that resolves to base64 encoded image
22
- */
23
- captureAndMaskWithOptions(options: MaskingOptions): Promise<string>
24
- }
25
-
26
- // Get the native module
27
- const { SessionRecorder } = NativeModules
28
-
29
- // Validate that the native module is available
30
- if (!SessionRecorder) {
31
- console.warn('SessionRecorder native module is not available. Auto-linking may not have completed yet.')
32
- }
33
-
34
- export default SessionRecorder as SessionRecorderNativeModule
@@ -1,34 +0,0 @@
1
- import { NativeModules } from 'react-native'
2
-
3
- export interface MaskingOptions {
4
- /** Quality of the captured image (0.1 to 1.0) */
5
- quality?: number
6
- /** Whether to mask all input fields automatically */
7
- inputMasking?: boolean
8
- }
9
-
10
-
11
- export interface SessionRecorderNativeModule {
12
- /**
13
- * Capture the current screen and apply masking to sensitive elements
14
- * @returns Promise that resolves to base64 encoded image
15
- */
16
- captureAndMask(): Promise<string>
17
-
18
- /**
19
- * Capture the current screen and apply masking with custom options
20
- * @param options Custom masking options
21
- * @returns Promise that resolves to base64 encoded image
22
- */
23
- captureAndMaskWithOptions(options: MaskingOptions): Promise<string>
24
- }
25
-
26
- // Get the native module
27
- const { SessionRecorderNative } = NativeModules
28
-
29
- // Validate that the native module is available
30
- if (!SessionRecorderNative) {
31
- console.warn('SessionRecorderNative module is not available. Auto-linking may not have completed yet.')
32
- }
33
-
34
- export default SessionRecorderNative as SessionRecorderNativeModule
@@ -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
- }