@multiplayer-app/session-recorder-react 2.0.17-alpha.9 → 2.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplayer-app/session-recorder-react",
3
- "version": "2.0.17-alpha.9",
3
+ "version": "2.0.18",
4
4
  "description": "Multiplayer Fullstack Session Recorder for React (browser wrapper)",
5
5
  "author": {
6
6
  "name": "Multiplayer Software, Inc.",
@@ -35,8 +35,8 @@
35
35
  "@opentelemetry/api": "^1.9.0"
36
36
  },
37
37
  "dependencies": {
38
- "@multiplayer-app/session-recorder-browser": "2.0.17-alpha.9",
39
- "@multiplayer-app/session-recorder-common": "2.0.17-alpha.9"
38
+ "@multiplayer-app/session-recorder-browser": "2.0.18",
39
+ "@multiplayer-app/session-recorder-common": "2.0.18"
40
40
  },
41
41
  "devDependencies": {
42
42
  "eslint": "10.1.0",
Binary file
@@ -1,38 +0,0 @@
1
- import React from 'react'
2
- import SessionRecorder from '@multiplayer-app/session-recorder-browser'
3
-
4
- export type ErrorBoundaryProps = {
5
- children: React.ReactNode
6
- /** Optional fallback UI to render when an error is caught */
7
- fallback?: React.ReactNode
8
- }
9
-
10
- type State = { hasError: boolean }
11
-
12
- export class ErrorBoundary extends React.Component<ErrorBoundaryProps, State> {
13
- constructor(props: ErrorBoundaryProps) {
14
- super(props)
15
- this.state = { hasError: false }
16
- }
17
-
18
- static getDerivedStateFromError(): State {
19
- return { hasError: true }
20
- }
21
-
22
- componentDidCatch(error: unknown, errorInfo: React.ErrorInfo): void {
23
- try {
24
- SessionRecorder.captureException(error, errorInfo)
25
- } catch (_e) {
26
- // no-op
27
- }
28
- }
29
-
30
- render(): React.ReactNode {
31
- if (this.state.hasError) {
32
- return this.props.fallback ?? null
33
- }
34
- return this.props.children
35
- }
36
- }
37
-
38
- export default ErrorBoundary
@@ -1,100 +0,0 @@
1
- import React, { createContext, useContext, useEffect, useCallback, type PropsWithChildren } from 'react'
2
- import SessionRecorder, { SessionState, SessionType } from '@multiplayer-app/session-recorder-browser'
3
- import { sessionRecorderStore } from './SessionRecorderStore'
4
-
5
- type SessionRecorderOptions = any
6
-
7
- interface SessionRecorderContextType {
8
- instance: typeof SessionRecorder
9
- startSession: (sessionType?: SessionType) => void | Promise<void>
10
- stopSession: (comment?: string) => Promise<void>
11
- pauseSession: () => Promise<void>
12
- resumeSession: () => Promise<void>
13
- cancelSession: () => Promise<void>
14
- saveSession: () => Promise<any>
15
- }
16
-
17
- const SessionRecorderContext = createContext<SessionRecorderContextType | null>(null)
18
-
19
- export interface SessionRecorderProviderProps extends PropsWithChildren {
20
- options?: SessionRecorderOptions
21
- }
22
-
23
- export const SessionRecorderProvider: React.FC<SessionRecorderProviderProps> = ({ children, options }) => {
24
- useEffect(() => {
25
- if (options) {
26
- SessionRecorder.init(options)
27
- }
28
- sessionRecorderStore.setState({ isInitialized: SessionRecorder.isInitialized })
29
- }, [])
30
-
31
- useEffect(() => {
32
- sessionRecorderStore.setState({
33
- sessionState: SessionRecorder.sessionState,
34
- sessionType: SessionRecorder.sessionType,
35
- })
36
-
37
- const onStateChange = (sessionState: SessionState) => {
38
- sessionRecorderStore.setState({ sessionState })
39
- }
40
- const onInit = () => {
41
- sessionRecorderStore.setState({ isInitialized: true })
42
- }
43
-
44
- SessionRecorder.on('state-change', onStateChange)
45
- SessionRecorder.on('init', onInit)
46
- return () => {
47
- SessionRecorder.off('state-change', onStateChange)
48
- SessionRecorder.off('init', onInit)
49
- }
50
- }, [])
51
-
52
- const startSession = useCallback((sessionType: SessionType = SessionType.MANUAL) => {
53
- return SessionRecorder.start(sessionType)
54
- }, [])
55
-
56
- const stopSession = useCallback((comment?: string) => {
57
- return SessionRecorder.stop(comment)
58
- }, [])
59
-
60
- const pauseSession = useCallback(() => {
61
- return SessionRecorder.pause()
62
- }, [])
63
-
64
- const resumeSession = useCallback(() => {
65
- return SessionRecorder.resume()
66
- }, [])
67
-
68
- const cancelSession = useCallback(() => {
69
- return SessionRecorder.cancel()
70
- }, [])
71
-
72
- const saveSession = useCallback(() => {
73
- return SessionRecorder.save()
74
- }, [])
75
-
76
- return (
77
- <SessionRecorderContext.Provider
78
- value={{
79
- instance: SessionRecorder,
80
- startSession,
81
- stopSession,
82
- pauseSession,
83
- resumeSession,
84
- cancelSession,
85
- saveSession,
86
- }}
87
- >
88
- {children}
89
- {/* No widget component here; consumer can import the browser widget if needed */}
90
- </SessionRecorderContext.Provider>
91
- )
92
- }
93
-
94
- export const useSessionRecorder = (): SessionRecorderContextType => {
95
- const context = useContext(SessionRecorderContext)
96
- if (!context) {
97
- throw new Error('useSessionRecorder must be used within a SessionRecorderProvider')
98
- }
99
- return context
100
- }
@@ -1,20 +0,0 @@
1
- import { createStore, type Store } from './createStore'
2
- import { SessionState, SessionType } from '@multiplayer-app/session-recorder-browser'
3
-
4
-
5
- export type SessionRecorderState = {
6
- isInitialized: boolean;
7
- sessionType: SessionType | null;
8
- sessionState: SessionState | null;
9
- isOnline: boolean;
10
- error: string | null;
11
- };
12
-
13
- export const sessionRecorderStore: Store<SessionRecorderState> =
14
- createStore<SessionRecorderState>({
15
- isInitialized: false,
16
- sessionType: null,
17
- sessionState: null,
18
- isOnline: true,
19
- error: null,
20
- })
@@ -1,37 +0,0 @@
1
- export type SessionRecorderState = {
2
- isInitialized: boolean
3
- sessionType: any | null
4
- sessionState: any | null
5
- isOnline: boolean
6
- error: string | null
7
- }
8
-
9
- type Listener<T> = (state: T, prev: T) => void
10
-
11
- export type Store<T> = {
12
- getState: () => T
13
- setState: (partial: Partial<T> | ((prev: T) => T)) => void
14
- subscribe: (listener: Listener<T>) => () => void
15
- }
16
-
17
- export function createStore<T extends object>(initialState: T): Store<T> {
18
- let state = initialState
19
- const listeners = new Set<Listener<T>>()
20
-
21
- const getState = () => state
22
-
23
- const setState: Store<T>['setState'] = (partial) => {
24
- const prev = state
25
- const next = typeof partial === 'function' ? (partial as (p: T) => T)(prev) : ({ ...prev, ...partial } as T)
26
- if (Object.is(next, prev)) return
27
- state = next
28
- listeners.forEach((l) => l(state, prev))
29
- }
30
-
31
- const subscribe: Store<T>['subscribe'] = (listener) => {
32
- listeners.add(listener)
33
- return () => listeners.delete(listener)
34
- }
35
-
36
- return { getState, setState, subscribe }
37
- }
@@ -1,47 +0,0 @@
1
- import { SessionState, SessionType } from '@multiplayer-app/session-recorder-browser'
2
-
3
- import {
4
- type SessionRecorderState,
5
- sessionRecorderStore,
6
- } from './SessionRecorderStore'
7
- import { useStoreSelector } from './useStoreSelector'
8
-
9
- /**
10
- * Select a derived slice from the shared Session Recorder store.
11
- * Works in both React (web) and React Native since the store shape is identical.
12
- *
13
- * @param selector - Function that maps the full store state to the slice you need
14
- * @param equalityFn - Optional comparator to avoid unnecessary re-renders
15
- * @returns The selected slice of state
16
- */
17
- export function useSessionRecorderStore<TSlice>(
18
- selector: (s: SessionRecorderState) => TSlice,
19
- equalityFn?: (a: TSlice, b: TSlice) => boolean,
20
- ): TSlice {
21
- return useStoreSelector<SessionRecorderState, TSlice>(
22
- sessionRecorderStore,
23
- selector,
24
- equalityFn,
25
- )
26
- }
27
-
28
- /**
29
- * Read the current session recording state (started, paused, stopped).
30
- */
31
- export function useSessionRecordingState() {
32
- return useSessionRecorderStore<SessionState | null>((s) => s.sessionState)
33
- }
34
-
35
- /**
36
- * Read the current session type (MANUAL/CONTINUOUS).
37
- */
38
- export function useSessionType() {
39
- return useSessionRecorderStore<SessionType | null>((s) => s.sessionType)
40
- }
41
-
42
- /**
43
- * Check whether the Session Recorder has been initialized.
44
- */
45
- export function useIsInitialized() {
46
- return useSessionRecorderStore<boolean>((s) => s.isInitialized)
47
- }
@@ -1,36 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react'
2
- import { type Store } from './createStore'
3
- import { shallowEqual } from '../utils/shallowEqual'
4
-
5
- export function useStoreSelector<TState extends object, TSlice>(
6
- store: Store<TState>,
7
- selector: (state: TState) => TSlice,
8
- equalityFn: (a: TSlice, b: TSlice) => boolean = Object.is,
9
- ): TSlice {
10
- const latestSelectorRef = useRef(selector)
11
- const latestEqualityRef = useRef(equalityFn)
12
- latestSelectorRef.current = selector
13
- latestEqualityRef.current = equalityFn
14
-
15
- const [slice, setSlice] = useState<TSlice>(() =>
16
- latestSelectorRef.current(store.getState()),
17
- )
18
-
19
- useEffect(() => {
20
- function handleChange(nextState: TState, prevState: TState) {
21
- const nextSlice = latestSelectorRef.current(nextState)
22
- const prevSlice = latestSelectorRef.current(prevState)
23
- if (!latestEqualityRef.current(nextSlice, prevSlice)) {
24
- setSlice(nextSlice)
25
- }
26
- }
27
- const unsubscribe = store.subscribe(handleChange)
28
- // Sync once in case changed between render and effect
29
- handleChange(store.getState(), store.getState())
30
- return unsubscribe
31
- }, [store])
32
-
33
- return slice
34
- }
35
-
36
- export const shallow = shallowEqual
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export * from '@multiplayer-app/session-recorder-browser'
2
- export { default } from '@multiplayer-app/session-recorder-browser'
3
-
4
- export * from './context/SessionRecorderContext'
5
- export * from './context/useSessionRecorderStore'
6
- export { ErrorBoundary } from './ErrorBoundary'
7
- export { useNavigationRecorder } from './navigation'
8
- export type { UseNavigationRecorderOptions } from './navigation'
package/src/navigation.ts DELETED
@@ -1,86 +0,0 @@
1
- import { useEffect, useRef } from 'react'
2
- import SessionRecorderBrowser, {
3
- NavigationSignal,
4
- } from '@multiplayer-app/session-recorder-browser'
5
-
6
- export interface UseNavigationRecorderOptions
7
- extends Partial<Omit<NavigationSignal, 'path' | 'timestamp'>> {
8
- /**
9
- * Overrides the path sent to the recorder. Defaults to the provided pathname argument.
10
- */
11
- path?: string
12
- /**
13
- * When true (default), document.title is captured if available.
14
- */
15
- captureDocumentTitle?: boolean
16
- }
17
-
18
- /**
19
- * React Router compatible navigation recorder hook.
20
- * Call inside a component where you can access current location and navigation events.
21
- * Example:
22
- * const location = useLocation();
23
- * useNavigationRecorder(location.pathname);
24
- */
25
- export function useNavigationRecorder(
26
- pathname: string,
27
- options?: UseNavigationRecorderOptions,
28
- ): void {
29
- const optionsRef = useRef(options)
30
- const hasRecordedInitialRef = useRef(false)
31
- const lastPathRef = useRef<string | null>(null)
32
-
33
- useEffect(() => {
34
- optionsRef.current = options
35
- }, [options])
36
-
37
- useEffect(() => {
38
- if (!pathname || !SessionRecorderBrowser?.navigation) {
39
- return
40
- }
41
-
42
- const resolvedOptions = optionsRef.current || {}
43
- const resolvedPath = resolvedOptions.path ?? pathname
44
-
45
- if (!resolvedPath) {
46
- return
47
- }
48
-
49
- if (lastPathRef.current === resolvedPath && !resolvedOptions.navigationType) {
50
- return
51
- }
52
-
53
- const captureDocumentTitle =
54
- resolvedOptions.captureDocumentTitle ?? true
55
-
56
- const signal: NavigationSignal = {
57
- path: resolvedPath,
58
- routeName: resolvedOptions.routeName ?? resolvedPath,
59
- title:
60
- resolvedOptions.title ??
61
- (captureDocumentTitle && typeof document !== 'undefined'
62
- ? document.title
63
- : undefined),
64
- url: resolvedOptions.url,
65
- params: resolvedOptions.params,
66
- state: resolvedOptions.state,
67
- navigationType:
68
- resolvedOptions.navigationType ??
69
- (hasRecordedInitialRef.current ? undefined : 'initial'),
70
- framework: resolvedOptions.framework ?? 'react',
71
- source: resolvedOptions.source ?? 'react-router',
72
- metadata: resolvedOptions.metadata,
73
- }
74
-
75
- try {
76
- SessionRecorderBrowser.navigation.record(signal)
77
- hasRecordedInitialRef.current = true
78
- lastPathRef.current = resolvedPath
79
- } catch (error) {
80
- if (process.env.NODE_ENV !== 'production') {
81
- // eslint-disable-next-line no-console
82
- console.warn('[SessionRecorder][React] Failed to record navigation', error)
83
- }
84
- }
85
- }, [pathname])
86
- }
@@ -1,20 +0,0 @@
1
- export function shallowEqual<T extends Record<string, any>>(
2
- a: T,
3
- b: T,
4
- ): boolean {
5
- if (Object.is(a, b)) return true
6
- if (!a || !b) return false
7
- const aKeys = Object.keys(a)
8
- const bKeys = Object.keys(b)
9
- if (aKeys.length !== bKeys.length) return false
10
- for (let i = 0; i < aKeys.length; i++) {
11
- const key = aKeys[i]
12
- if (
13
- !Object.prototype.hasOwnProperty.call(b, key!) ||
14
- !Object.is(a[key!], b[key!])
15
- ) {
16
- return false
17
- }
18
- }
19
- return true
20
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "Node",
6
- "declaration": true,
7
- "declarationMap": true,
8
- "sourceMap": true,
9
- "outDir": "dist",
10
- "strict": true,
11
- "esModuleInterop": true,
12
- "skipLibCheck": true,
13
- "forceConsistentCasingInFileNames": true,
14
- "jsx": "react-jsx",
15
- "types": ["react"]
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist"]
19
- }