@multiplayer-app/session-recorder-react-native 0.0.1-beta.7 → 0.0.1-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.plugin.js +42 -0
- package/docs/NATIVE_MODULE_SETUP.md +177 -0
- package/ios/SessionRecorderNative.podspec +5 -0
- package/package.json +10 -1
- package/plugin/package.json +20 -0
- package/plugin/src/index.js +42 -0
- package/android/src/main/AndroidManifest.xml +0 -2
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +0 -202
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +0 -16
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +0 -202
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +0 -16
- package/babel.config.js +0 -13
- package/docs/AUTO_METADATA_DETECTION.md +0 -108
- package/docs/TROUBLESHOOTING.md +0 -168
- package/ios/ScreenMasking.m +0 -12
- package/ios/ScreenMasking.podspec +0 -21
- package/ios/ScreenMasking.swift +0 -205
- package/ios/SessionRecorder.podspec +0 -21
- package/scripts/generate-app-metadata.js +0 -173
- package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +0 -86
- package/src/components/GestureCaptureWrapper/index.ts +0 -1
- package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +0 -72
- package/src/components/ScreenRecorderView/index.ts +0 -1
- package/src/components/SessionRecorderWidget/FinalPopover.tsx +0 -62
- package/src/components/SessionRecorderWidget/FloatingButton.tsx +0 -136
- package/src/components/SessionRecorderWidget/InitialPopover.tsx +0 -89
- package/src/components/SessionRecorderWidget/ModalContainer.tsx +0 -128
- package/src/components/SessionRecorderWidget/ModalHeader.tsx +0 -24
- package/src/components/SessionRecorderWidget/SessionRecorderWidget.tsx +0 -109
- package/src/components/SessionRecorderWidget/icons.tsx +0 -52
- package/src/components/SessionRecorderWidget/index.ts +0 -3
- package/src/components/SessionRecorderWidget/styles.ts +0 -150
- package/src/components/index.ts +0 -3
- package/src/config/constants.ts +0 -60
- package/src/config/defaults.ts +0 -83
- package/src/config/index.ts +0 -6
- package/src/config/masking.ts +0 -28
- package/src/config/session-recorder.ts +0 -55
- package/src/config/validators.ts +0 -31
- package/src/context/SessionRecorderContext.tsx +0 -53
- package/src/index.ts +0 -9
- package/src/native/ScreenMasking.ts +0 -34
- package/src/native/SessionRecorderNative.ts +0 -34
- package/src/otel/helpers.ts +0 -275
- package/src/otel/index.ts +0 -138
- package/src/otel/instrumentations/index.ts +0 -115
- package/src/patch/index.ts +0 -1
- package/src/patch/xhr.ts +0 -141
- package/src/recorder/eventExporter.ts +0 -141
- package/src/recorder/gestureRecorder.ts +0 -498
- package/src/recorder/index.ts +0 -179
- package/src/recorder/navigationTracker.ts +0 -449
- package/src/recorder/screenRecorder.ts +0 -527
- package/src/services/api.service.ts +0 -203
- package/src/services/screenMaskingService.ts +0 -118
- package/src/services/storage.service.ts +0 -199
- package/src/session-recorder.ts +0 -606
- package/src/types/expo.d.ts +0 -23
- package/src/types/index.ts +0 -28
- package/src/types/session-recorder.ts +0 -429
- package/src/types/session.ts +0 -65
- package/src/utils/app-metadata.ts +0 -31
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.ts +0 -225
- package/src/utils/nativeModuleTest.ts +0 -60
- package/src/utils/platform.ts +0 -384
- package/src/utils/request-utils.ts +0 -61
- package/src/utils/rrweb-events.ts +0 -309
- package/src/utils/session.ts +0 -18
- package/src/utils/time.ts +0 -17
- package/src/utils/type-utils.ts +0 -75
- package/src/version.ts +0 -1
- package/tsconfig.json +0 -24
- /package/ios/{SessionRecorder.m → SessionRecorderNative.m} +0 -0
- /package/ios/{SessionRecorder.swift → SessionRecorderNative.swift} +0 -0
package/src/components/index.ts
DELETED
package/src/config/constants.ts
DELETED
|
@@ -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
|
-
]
|
package/src/config/defaults.ts
DELETED
|
@@ -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
|
-
}
|
package/src/config/index.ts
DELETED
package/src/config/masking.ts
DELETED
|
@@ -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
|
-
}
|
package/src/config/validators.ts
DELETED
|
@@ -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
|
package/src/otel/helpers.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { Span } from '@opentelemetry/api'
|
|
2
|
-
import {
|
|
3
|
-
MULTIPLAYER_TRACE_DEBUG_PREFIX,
|
|
4
|
-
MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX,
|
|
5
|
-
ATTR_MULTIPLAYER_HTTP_REQUEST_BODY,
|
|
6
|
-
ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS,
|
|
7
|
-
ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY,
|
|
8
|
-
ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS,
|
|
9
|
-
} from '@multiplayer-app/session-recorder-common'
|
|
10
|
-
import { logger } from '../utils'
|
|
11
|
-
import { SessionRecorderSdk } from '@multiplayer-app/session-recorder-common'
|
|
12
|
-
import { TracerReactNativeConfig } from '../types'
|
|
13
|
-
|
|
14
|
-
const { schemify } = SessionRecorderSdk
|
|
15
|
-
|
|
16
|
-
export interface HttpPayloadData {
|
|
17
|
-
requestBody?: any
|
|
18
|
-
responseBody?: any
|
|
19
|
-
requestHeaders?: Record<string, string>
|
|
20
|
-
responseHeaders?: Record<string, string>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ProcessedHttpPayload {
|
|
24
|
-
requestBody?: string
|
|
25
|
-
responseBody?: string
|
|
26
|
-
requestHeaders?: string
|
|
27
|
-
responseHeaders?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Checks if the trace should be processed based on trace ID prefixes
|
|
32
|
-
*/
|
|
33
|
-
export function shouldProcessTrace(traceId: string): boolean {
|
|
34
|
-
return (
|
|
35
|
-
traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
|
36
|
-
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Processes request and response body based on trace type and configuration
|
|
42
|
-
*/
|
|
43
|
-
export function processBody(
|
|
44
|
-
payload: HttpPayloadData,
|
|
45
|
-
config: TracerReactNativeConfig,
|
|
46
|
-
span: Span,
|
|
47
|
-
): { requestBody?: string; responseBody?: string } {
|
|
48
|
-
const { captureBody, masking } = config
|
|
49
|
-
const traceId = span.spanContext().traceId
|
|
50
|
-
|
|
51
|
-
if (!captureBody) {
|
|
52
|
-
return {}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let { requestBody, responseBody } = payload
|
|
56
|
-
|
|
57
|
-
if (requestBody !== undefined && requestBody !== null) {
|
|
58
|
-
requestBody = JSON.parse(JSON.stringify(requestBody))
|
|
59
|
-
}
|
|
60
|
-
if (responseBody !== undefined && responseBody !== null) {
|
|
61
|
-
responseBody = JSON.parse(JSON.stringify(responseBody))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Apply masking for debug traces
|
|
65
|
-
if (
|
|
66
|
-
traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
|
67
|
-
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
|
68
|
-
) {
|
|
69
|
-
if (masking.isContentMaskingEnabled) {
|
|
70
|
-
requestBody = requestBody && masking.maskBody?.(requestBody, span)
|
|
71
|
-
responseBody = responseBody && masking.maskBody?.(responseBody, span)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Convert to string if needed
|
|
76
|
-
if (typeof requestBody !== 'string') {
|
|
77
|
-
requestBody = JSON.stringify(requestBody)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (typeof responseBody !== 'string') {
|
|
81
|
-
responseBody = JSON.stringify(responseBody)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
requestBody: requestBody?.length ? requestBody : undefined,
|
|
86
|
-
responseBody: responseBody?.length ? responseBody : undefined,
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Processes request and response headers based on configuration
|
|
92
|
-
*/
|
|
93
|
-
export function processHeaders(
|
|
94
|
-
payload: HttpPayloadData,
|
|
95
|
-
config: TracerReactNativeConfig,
|
|
96
|
-
span: Span,
|
|
97
|
-
): { requestHeaders?: string; responseHeaders?: string } {
|
|
98
|
-
const { captureHeaders, masking } = config
|
|
99
|
-
|
|
100
|
-
if (!captureHeaders) {
|
|
101
|
-
return {}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let { requestHeaders = {}, responseHeaders = {} } = payload
|
|
105
|
-
|
|
106
|
-
// Handle header filtering
|
|
107
|
-
if (
|
|
108
|
-
!masking.headersToInclude?.length &&
|
|
109
|
-
!masking.headersToExclude?.length
|
|
110
|
-
) {
|
|
111
|
-
// Add null checks to prevent JSON.parse error when headers is undefined
|
|
112
|
-
if (requestHeaders !== undefined && requestHeaders !== null) {
|
|
113
|
-
requestHeaders = JSON.parse(JSON.stringify(requestHeaders))
|
|
114
|
-
}
|
|
115
|
-
if (responseHeaders !== undefined && responseHeaders !== null) {
|
|
116
|
-
responseHeaders = JSON.parse(JSON.stringify(responseHeaders))
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
if (masking.headersToInclude) {
|
|
120
|
-
const _requestHeaders: Record<string, string> = {}
|
|
121
|
-
const _responseHeaders: Record<string, string> = {}
|
|
122
|
-
|
|
123
|
-
for (const headerName of masking.headersToInclude) {
|
|
124
|
-
if (requestHeaders[headerName]) {
|
|
125
|
-
_requestHeaders[headerName] = requestHeaders[headerName]
|
|
126
|
-
}
|
|
127
|
-
if (responseHeaders[headerName]) {
|
|
128
|
-
_responseHeaders[headerName] = responseHeaders[headerName]
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
requestHeaders = _requestHeaders
|
|
133
|
-
responseHeaders = _responseHeaders
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (masking.headersToExclude?.length) {
|
|
137
|
-
for (const headerName of masking.headersToExclude) {
|
|
138
|
-
delete requestHeaders[headerName]
|
|
139
|
-
delete responseHeaders[headerName]
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Apply masking
|
|
145
|
-
const maskedRequestHeaders = masking.maskHeaders?.(requestHeaders, span) || requestHeaders
|
|
146
|
-
const maskedResponseHeaders = masking.maskHeaders?.(responseHeaders, span) || responseHeaders
|
|
147
|
-
|
|
148
|
-
// Convert to string
|
|
149
|
-
const requestHeadersStr = typeof maskedRequestHeaders === 'string'
|
|
150
|
-
? maskedRequestHeaders
|
|
151
|
-
: JSON.stringify(maskedRequestHeaders)
|
|
152
|
-
|
|
153
|
-
const responseHeadersStr = typeof maskedResponseHeaders === 'string'
|
|
154
|
-
? maskedResponseHeaders
|
|
155
|
-
: JSON.stringify(maskedResponseHeaders)
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
requestHeaders: requestHeadersStr?.length ? requestHeadersStr : undefined,
|
|
159
|
-
responseHeaders: responseHeadersStr?.length ? responseHeadersStr : undefined,
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Processes HTTP payload (body and headers) and sets span attributes
|
|
165
|
-
*/
|
|
166
|
-
export function processHttpPayload(
|
|
167
|
-
payload: HttpPayloadData,
|
|
168
|
-
config: TracerReactNativeConfig,
|
|
169
|
-
span: Span,
|
|
170
|
-
): void {
|
|
171
|
-
const traceId = span.spanContext().traceId
|
|
172
|
-
|
|
173
|
-
if (!shouldProcessTrace(traceId)) {
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const { requestBody, responseBody } = processBody(payload, config, span)
|
|
178
|
-
const { requestHeaders, responseHeaders } = processHeaders(payload, config, span)
|
|
179
|
-
|
|
180
|
-
// Set span attributes
|
|
181
|
-
if (requestBody) {
|
|
182
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_BODY, requestBody)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (responseBody) {
|
|
186
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY, responseBody)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (requestHeaders) {
|
|
190
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS, requestHeaders)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (responseHeaders) {
|
|
194
|
-
span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, responseHeaders)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Converts Headers object to plain object
|
|
200
|
-
*/
|
|
201
|
-
export function headersToObject(headers: Headers | Record<string, string> | Record<string, string | string[]> | string[][] | undefined): Record<string, string> {
|
|
202
|
-
const result: Record<string, string> = {}
|
|
203
|
-
|
|
204
|
-
if (!headers) {
|
|
205
|
-
return result
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (headers instanceof Headers) {
|
|
209
|
-
headers.forEach((value: string, key: string) => {
|
|
210
|
-
result[key] = value
|
|
211
|
-
})
|
|
212
|
-
} else if (Array.isArray(headers)) {
|
|
213
|
-
// Handle array of [key, value] pairs
|
|
214
|
-
for (const [key, value] of headers) {
|
|
215
|
-
if (typeof key === 'string' && typeof value === 'string') {
|
|
216
|
-
result[key] = value
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} else if (typeof headers === 'object' && !Array.isArray(headers)) {
|
|
220
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
221
|
-
if (typeof key === 'string' && typeof value === 'string') {
|
|
222
|
-
result[key] = value
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return result
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Extracts response body as string from Response object
|
|
232
|
-
*/
|
|
233
|
-
export async function extractResponseBody(response: Response): Promise<string | null> {
|
|
234
|
-
if (!response.body) {
|
|
235
|
-
return null
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
if (response.body instanceof ReadableStream) {
|
|
240
|
-
// Check if response body is already consumed
|
|
241
|
-
if (response.bodyUsed) {
|
|
242
|
-
return null
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const responseClone = response.clone()
|
|
246
|
-
return responseClone.text()
|
|
247
|
-
} else {
|
|
248
|
-
return JSON.stringify(response.body)
|
|
249
|
-
}
|
|
250
|
-
} catch (error) {
|
|
251
|
-
// If cloning fails (body already consumed), return null
|
|
252
|
-
// eslint-disable-next-line no-console
|
|
253
|
-
logger.warn('DEBUGGER_LIB', 'Failed to extract response body', error)
|
|
254
|
-
return null
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export const getExporterEndpoint = (exporterEndpoint: string): string => {
|
|
259
|
-
const hasPath = exporterEndpoint && (() => {
|
|
260
|
-
try {
|
|
261
|
-
const url = new URL(exporterEndpoint)
|
|
262
|
-
return url.pathname !== '/' && url.pathname !== ''
|
|
263
|
-
} catch {
|
|
264
|
-
return false
|
|
265
|
-
}
|
|
266
|
-
})()
|
|
267
|
-
|
|
268
|
-
if (hasPath) {
|
|
269
|
-
return exporterEndpoint
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const trimmedExporterEndpoint = new URL(exporterEndpoint).origin
|
|
273
|
-
|
|
274
|
-
return `${trimmedExporterEndpoint}/v1/traces`
|
|
275
|
-
}
|