@multiplayer-app/session-recorder-react 1.2.15
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/README.md +386 -0
- package/dist/context/SessionRecorderContext.d.ts +19 -0
- package/dist/context/SessionRecorderContext.d.ts.map +1 -0
- package/dist/context/SessionRecorderContext.js +66 -0
- package/dist/context/SessionRecorderContext.js.map +1 -0
- package/dist/context/SessionRecorderStore.d.ts +11 -0
- package/dist/context/SessionRecorderStore.d.ts.map +1 -0
- package/dist/context/SessionRecorderStore.js +9 -0
- package/dist/context/SessionRecorderStore.js.map +1 -0
- package/dist/context/createStore.d.ts +16 -0
- package/dist/context/createStore.d.ts.map +1 -0
- package/dist/context/createStore.js +19 -0
- package/dist/context/createStore.js.map +1 -0
- package/dist/context/useSessionRecorderStore.d.ts +24 -0
- package/dist/context/useSessionRecorderStore.d.ts.map +1 -0
- package/dist/context/useSessionRecorderStore.js +32 -0
- package/dist/context/useSessionRecorderStore.js.map +1 -0
- package/dist/context/useStoreSelector.d.ts +5 -0
- package/dist/context/useStoreSelector.d.ts.map +1 -0
- package/dist/context/useStoreSelector.js +25 -0
- package/dist/context/useStoreSelector.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation.d.ts +20 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +59 -0
- package/dist/navigation.js.map +1 -0
- package/dist/utils/shallowEqual.d.ts +2 -0
- package/dist/utils/shallowEqual.d.ts.map +1 -0
- package/dist/utils/shallowEqual.js +19 -0
- package/dist/utils/shallowEqual.js.map +1 -0
- package/docs/img/header-js.png +0 -0
- package/package.json +46 -0
- package/src/context/SessionRecorderContext.tsx +100 -0
- package/src/context/SessionRecorderStore.ts +20 -0
- package/src/context/createStore.ts +37 -0
- package/src/context/useSessionRecorderStore.ts +47 -0
- package/src/context/useStoreSelector.ts +36 -0
- package/src/index.ts +10 -0
- package/src/navigation.ts +86 -0
- package/src/utils/shallowEqual.ts +20 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="https://github.com/multiplayer-app/multiplayer-session-recorder-javascript">
|
|
5
|
+
<img src="https://img.shields.io/github/stars/multiplayer-app/multiplayer-session-recorder-javascript?style=social&label=Star&maxAge=2592000" alt="GitHub stars">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://github.com/multiplayer-app/multiplayer-session-recorder-javascript/blob/main/LICENSE">
|
|
8
|
+
<img src="https://img.shields.io/github/license/multiplayer-app/multiplayer-session-recorder-javascript" alt="License">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://multiplayer.app">
|
|
11
|
+
<img src="https://img.shields.io/badge/Visit-multiplayer.app-blue" alt="Visit Multiplayer">
|
|
12
|
+
</a>
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://x.com/trymultiplayer">
|
|
18
|
+
<img src="https://img.shields.io/badge/Follow%20on%20X-000000?style=for-the-badge&logo=x&logoColor=white" alt="Follow on X" />
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://www.linkedin.com/company/multiplayer-app/">
|
|
21
|
+
<img src="https://img.shields.io/badge/Follow%20on%20LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white" alt="Follow on LinkedIn" />
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://discord.com/invite/q9K3mDzfrx">
|
|
24
|
+
<img src="https://img.shields.io/badge/Join%20our%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord" />
|
|
25
|
+
</a>
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
# Multiplayer Session Recorder React
|
|
30
|
+
|
|
31
|
+
React bindings for the [Multiplayer Full Stack Session Recorder](../session-recorder-browser/README.md).
|
|
32
|
+
|
|
33
|
+
Use this wrapper to wire the browser SDK into your React or Next.js application with idiomatic hooks, context helpers, and navigation tracking.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @multiplayer-app/session-recorder-react @opentelemetry/api
|
|
39
|
+
# or
|
|
40
|
+
yarn add @multiplayer-app/session-recorder-react @opentelemetry/api
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To get full‑stack session recording working, set up one of our backend SDKs/CLI apps:
|
|
44
|
+
|
|
45
|
+
- [Node.js](https://github.com/multiplayer-app/multiplayer-session-recorder-javascript/tree/main/packages/session-recorder-node)
|
|
46
|
+
- [Python](https://github.com/multiplayer-app/multiplayer-session-recorder-python?tab=readme-ov-file)
|
|
47
|
+
- [Java](https://github.com/multiplayer-app/multiplayer-session-recorder-java?tab=readme-ov-file)
|
|
48
|
+
- [.NET](https://github.com/multiplayer-app/multiplayer-session-recorder-dotnet?tab=readme-ov-file)
|
|
49
|
+
- [Go](https://github.com/multiplayer-app/multiplayer-session-recorder-go?tab=readme-ov-file)
|
|
50
|
+
- [Ruby](https://github.com/multiplayer-app/multiplayer-session-recorder-ruby?tab=readme-ov-file)
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
1. Wrap your application with the `SessionRecorderProvider`.
|
|
55
|
+
2. Pass the same configuration you would supply to the browser SDK.
|
|
56
|
+
3. Start or stop sessions using the widget or the provided hooks.
|
|
57
|
+
|
|
58
|
+
### Minimal setup with manual initialization
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// src/main.tsx or src/index.tsx app root
|
|
62
|
+
import React from 'react'
|
|
63
|
+
import ReactDOM from 'react-dom/client'
|
|
64
|
+
import App from './App'
|
|
65
|
+
import { SessionRecorderProvider } from '@multiplayer-app/session-recorder-react'
|
|
66
|
+
|
|
67
|
+
const sessionRecorderConfig = {
|
|
68
|
+
version: '1.0.0',
|
|
69
|
+
environment: 'production',
|
|
70
|
+
application: 'my-react-app',
|
|
71
|
+
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
|
|
72
|
+
// IMPORTANT: in order to propagate OTLP headers to a backend
|
|
73
|
+
// domain(s) with a different origin, add backend domain(s) below.
|
|
74
|
+
// e.g. if you serve your website from www.example.com
|
|
75
|
+
// and your backend domain is at api.example.com set value as shown below:
|
|
76
|
+
// format: string|RegExp|Array
|
|
77
|
+
propagateTraceHeaderCorsUrls: [new RegExp('https://api.example.com', 'i')]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Initialize the session recorder manually
|
|
81
|
+
SessionRecorderProvider.init(sessionRecorderConfig)
|
|
82
|
+
|
|
83
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
84
|
+
<SessionRecorderProvider>
|
|
85
|
+
<App />
|
|
86
|
+
</SessionRecorderProvider>
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Minimal setup with provider initialization
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// src/main.tsx or src/index.tsx app root
|
|
94
|
+
import React from 'react'
|
|
95
|
+
import ReactDOM from 'react-dom/client'
|
|
96
|
+
import App from './App'
|
|
97
|
+
import { SessionRecorderProvider } from '@multiplayer-app/session-recorder-react'
|
|
98
|
+
|
|
99
|
+
const sessionRecorderConfig = {
|
|
100
|
+
version: '1.0.0',
|
|
101
|
+
environment: 'production',
|
|
102
|
+
application: 'my-react-app',
|
|
103
|
+
apiKey: 'YOUR_MULTIPLAYER_API_KEY'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
107
|
+
<SessionRecorderProvider options={sessionRecorderConfig}>
|
|
108
|
+
<App />
|
|
109
|
+
</SessionRecorderProvider>
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Behind the scenes, the provider initializes the shared Browser SDK (if you pass the configuration as options to the provider) — or you can initialize it manually as shown in the example above. It then sets up listeners and exposes helper APIs through React context and selectors.
|
|
114
|
+
|
|
115
|
+
### Set session attributes to provide context for the session
|
|
116
|
+
|
|
117
|
+
Use session attributes to attach user context to recordings. The provided `userName` and `userId` will be visible in the Multiplayer sessions list and in the session details (shown as the reporter), making it easier to identify who reported or recorded the session.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { useEffect } from 'react'
|
|
121
|
+
import SessionRecorder from '@multiplayer-app/session-recorder-react'
|
|
122
|
+
//... your code
|
|
123
|
+
|
|
124
|
+
const MyComponent = () => {
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
SessionRecorder.setSessionAttributes({
|
|
127
|
+
userId: '12345', // replace with your user id
|
|
128
|
+
userName: 'John Doe' // replace with your user name
|
|
129
|
+
})
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
//... your code
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//... your code
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Using without the built‑in widget (imperative‑only)
|
|
139
|
+
|
|
140
|
+
If you prefer not to render our floating widget, disable it and rely purely on the imperative hooks. Use the context hook when you need imperative control (for example, to bind to buttons or QA tooling) as shown in the example below:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// Provider configuration
|
|
144
|
+
<SessionRecorderProvider
|
|
145
|
+
options={{
|
|
146
|
+
application: 'my-react-app',
|
|
147
|
+
version: '1.0.0',
|
|
148
|
+
environment: 'production',
|
|
149
|
+
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
|
|
150
|
+
showWidget: false // hide the built-in widget
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<App />
|
|
154
|
+
</SessionRecorderProvider>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Conditional controls with state (recommended UX)
|
|
158
|
+
|
|
159
|
+
Create your own UI and wire it to the hook methods. Render only the relevant actions based on the current session state (e.g., show Stop only when recording is started):
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import React from 'react'
|
|
163
|
+
import { useSessionRecorder, useSessionRecordingState, SessionState, SessionType } from '@multiplayer-app/session-recorder-react'
|
|
164
|
+
|
|
165
|
+
export function SmartSessionControls() {
|
|
166
|
+
const { startSession, stopSession, pauseSession, resumeSession } = useSessionRecorder()
|
|
167
|
+
const sessionState = useSessionRecordingState()
|
|
168
|
+
|
|
169
|
+
const isStarted = sessionState === SessionState.started
|
|
170
|
+
const isPaused = sessionState === SessionState.paused
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div>
|
|
174
|
+
{/* Idle state: allow starting */}
|
|
175
|
+
{!isStarted && !isPaused && (
|
|
176
|
+
<>
|
|
177
|
+
<button onClick={() => startSession()}>Start</button>
|
|
178
|
+
<button onClick={() => startSession(SessionType.CONTINUOUS)}>Start Continuous</button>
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Started state: allow pause or stop */}
|
|
183
|
+
{isStarted && (
|
|
184
|
+
<>
|
|
185
|
+
<button onClick={() => pauseSession()}>Pause</button>
|
|
186
|
+
<button onClick={() => stopSession('Finished recording')}>Stop</button>
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Paused state: allow resume or stop */}
|
|
191
|
+
{isPaused && (
|
|
192
|
+
<>
|
|
193
|
+
<button onClick={() => resumeSession()}>Resume</button>
|
|
194
|
+
<button onClick={() => stopSession('Finished recording')}>Stop</button>
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Reading recorder state with selectors
|
|
203
|
+
|
|
204
|
+
The package ships a lightweight observable store that mirrors the browser SDK. Use the selectors to drive UI state without forcing rerenders on unrelated updates.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import React from 'react'
|
|
208
|
+
import {
|
|
209
|
+
useSessionRecordingState,
|
|
210
|
+
useSessionType,
|
|
211
|
+
useIsInitialized,
|
|
212
|
+
SessionState,
|
|
213
|
+
SessionType
|
|
214
|
+
} from '@multiplayer-app/session-recorder-react'
|
|
215
|
+
|
|
216
|
+
export function RecorderStatusBanner() {
|
|
217
|
+
const isReady = useIsInitialized()
|
|
218
|
+
const sessionState = useSessionRecordingState()
|
|
219
|
+
const sessionType = useSessionType()
|
|
220
|
+
|
|
221
|
+
if (!isReady) {
|
|
222
|
+
return <span>Session recorder initializing…</span>
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<span>
|
|
227
|
+
State: {sessionState ?? SessionState.stopped} | Mode: {sessionType ?? SessionType.MANUAL}
|
|
228
|
+
</span>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Recording navigation in React apps
|
|
234
|
+
|
|
235
|
+
The Session Recorder React package includes a `useNavigationRecorder` hook that forwards router changes to the shared navigation recorder. Attach it inside your routing layer to correlate screen changes with traces and replays.
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// React Router v7/v6
|
|
239
|
+
import { useLocation, useNavigationType } from 'react-router-dom'
|
|
240
|
+
import { useNavigationRecorder } from '@multiplayer-app/session-recorder-react'
|
|
241
|
+
|
|
242
|
+
export function NavigationTracker() {
|
|
243
|
+
const location = useLocation()
|
|
244
|
+
const navigationType = useNavigationType()
|
|
245
|
+
|
|
246
|
+
useNavigationRecorder(location.pathname, {
|
|
247
|
+
navigationType,
|
|
248
|
+
params: location.state as Record<string, unknown> | undefined
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// React Router v5 (older)
|
|
257
|
+
import { useLocation, useHistory } from 'react-router-dom'
|
|
258
|
+
import { useNavigationRecorder } from '@multiplayer-app/session-recorder-react'
|
|
259
|
+
|
|
260
|
+
export function NavigationTrackerLegacy() {
|
|
261
|
+
const location = useLocation()
|
|
262
|
+
const history = useHistory()
|
|
263
|
+
|
|
264
|
+
// PUSH | REPLACE | POP => push | replace | pop
|
|
265
|
+
const navigationType = (history.action || 'PUSH').toLowerCase()
|
|
266
|
+
|
|
267
|
+
useNavigationRecorder(location.pathname, {
|
|
268
|
+
navigationType,
|
|
269
|
+
params: location.state as Record<string, unknown> | undefined
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Advanced navigation metadata
|
|
277
|
+
|
|
278
|
+
`useNavigationRecorder` accepts an options object allowing you to override the detected `path`, attach custom `routeName`, include query params, or disable document title capture. For full control you can call `SessionRecorder.navigation.record({ ... })` directly using the shared browser instance exported by this package.
|
|
279
|
+
|
|
280
|
+
## Configuration reference
|
|
281
|
+
|
|
282
|
+
The `options` prop passed to `SessionRecorderProvider` is forwarded to the underlying browser SDK. Refer to the [browser README](../session-recorder-browser/README.md#initialize) for the full option list, including:
|
|
283
|
+
|
|
284
|
+
- `application`, `version`, `environment`, `apiKey`
|
|
285
|
+
- `showWidget`, `showContinuousRecording`
|
|
286
|
+
- `recordNavigation`, `recordCanvas`, `recordGestures`
|
|
287
|
+
- `propagateTraceHeaderCorsUrls`, `ignoreUrls`
|
|
288
|
+
- `masking`, `captureBody`, `captureHeaders`
|
|
289
|
+
- `maxCapturingHttpPayloadSize` and other advanced HTTP controls
|
|
290
|
+
|
|
291
|
+
Any time `recordNavigation` is enabled, the browser SDK will emit OpenTelemetry navigation spans and keep an in-memory stack of visited routes. You can access the navigation helpers through `SessionRecorder.navigation` if you need to introspect from React components.
|
|
292
|
+
|
|
293
|
+
## Next.js integration tips
|
|
294
|
+
|
|
295
|
+
- Initialize the provider in a Client Component (for example `app/providers.tsx`) because the browser SDK requires `window`.
|
|
296
|
+
- In the App Router, render the `SessionRecorderProvider` at the top of `app/layout.tsx` and add the `NavigationTracker` component inside your root layout so every route change is captured.
|
|
297
|
+
- If your frontend calls APIs on different origins, set `propagateTraceHeaderCorsUrls` so backend traces correlate correctly.
|
|
298
|
+
|
|
299
|
+
### Next.js support (coming soon) and temporary solution
|
|
300
|
+
|
|
301
|
+
An official Next.js-specific wrapper is coming soon. Until then, you can use this package safely in Next.js by:
|
|
302
|
+
|
|
303
|
+
1. Initializing in a Client Component
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
'use client'
|
|
307
|
+
import React from 'react'
|
|
308
|
+
import { SessionRecorderProvider } from '@multiplayer-app/session-recorder-react'
|
|
309
|
+
|
|
310
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
311
|
+
return (
|
|
312
|
+
<SessionRecorderProvider
|
|
313
|
+
options={{
|
|
314
|
+
application: 'my-next-app',
|
|
315
|
+
version: '1.0.0',
|
|
316
|
+
environment: 'production',
|
|
317
|
+
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
|
|
318
|
+
showWidget: true
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
{children}
|
|
322
|
+
</SessionRecorderProvider>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
2. Tracking navigation (App Router)
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
'use client'
|
|
331
|
+
import { usePathname, useSearchParams } from 'next/navigation'
|
|
332
|
+
import { useNavigationRecorder } from '@multiplayer-app/session-recorder-react'
|
|
333
|
+
|
|
334
|
+
export function NavigationTracker() {
|
|
335
|
+
const pathname = usePathname()
|
|
336
|
+
const searchParams = useSearchParams()
|
|
337
|
+
|
|
338
|
+
// Convert search params to an object for richer metadata
|
|
339
|
+
const params = Object.fromEntries(searchParams?.entries?.() ?? [])
|
|
340
|
+
|
|
341
|
+
// Hook records whenever pathname changes (query changes included via params)
|
|
342
|
+
useNavigationRecorder(pathname || '/', {
|
|
343
|
+
params,
|
|
344
|
+
framework: 'nextjs',
|
|
345
|
+
source: 'next/navigation'
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
3. Tracking navigation (Pages Router, older)
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
'use client'
|
|
356
|
+
import { useRouter } from 'next/router'
|
|
357
|
+
import { useNavigationRecorder } from '@multiplayer-app/session-recorder-react'
|
|
358
|
+
|
|
359
|
+
export function NavigationTrackerLegacy() {
|
|
360
|
+
const { asPath, query } = useRouter()
|
|
361
|
+
const pathname = asPath.split('?')[0]
|
|
362
|
+
|
|
363
|
+
useNavigationRecorder(pathname, {
|
|
364
|
+
params: query,
|
|
365
|
+
framework: 'nextjs',
|
|
366
|
+
source: 'next/router'
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## TypeScript support
|
|
374
|
+
|
|
375
|
+
All hooks and helpers ship with TypeScript types. To extend the navigation metadata, annotate the `params` or `metadata` properties in your own app code. The package re-exports all relevant browser SDK types for convenience.
|
|
376
|
+
|
|
377
|
+
## Troubleshooting
|
|
378
|
+
|
|
379
|
+
- Ensure the provider wraps your entire component tree so context hooks resolve.
|
|
380
|
+
- Confirm `SessionRecorder.init` runs only once. The provider handles this automatically if you pass the configuration as options to the provider; do not call it manually elsewhere.
|
|
381
|
+
- Ensure the session recorder required options are passed and the API key is valid.
|
|
382
|
+
- For SSR environments, guard any direct `document` or `window` usage behind `typeof window !== 'undefined'` checks (the helper hooks already do this).
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
Distributed under the [MIT License](../session-recorder-browser/LICENSE).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, { type PropsWithChildren } from 'react';
|
|
2
|
+
import SessionRecorder, { SessionType } from '@multiplayer-app/session-recorder-browser';
|
|
3
|
+
type SessionRecorderOptions = any;
|
|
4
|
+
interface SessionRecorderContextType {
|
|
5
|
+
instance: typeof SessionRecorder;
|
|
6
|
+
startSession: (sessionType?: SessionType) => void | Promise<void>;
|
|
7
|
+
stopSession: (comment?: string) => Promise<void>;
|
|
8
|
+
pauseSession: () => Promise<void>;
|
|
9
|
+
resumeSession: () => Promise<void>;
|
|
10
|
+
cancelSession: () => Promise<void>;
|
|
11
|
+
saveSession: () => Promise<any>;
|
|
12
|
+
}
|
|
13
|
+
export interface SessionRecorderProviderProps extends PropsWithChildren {
|
|
14
|
+
options?: SessionRecorderOptions;
|
|
15
|
+
}
|
|
16
|
+
export declare const SessionRecorderProvider: React.FC<SessionRecorderProviderProps>;
|
|
17
|
+
export declare const useSessionRecorder: () => SessionRecorderContextType;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=SessionRecorderContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionRecorderContext.d.ts","sourceRoot":"","sources":["../../src/context/SessionRecorderContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAqD,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAA;AACxG,OAAO,eAAe,EAAE,EAAgB,WAAW,EAAE,MAAM,2CAA2C,CAAA;AAGtG,KAAK,sBAAsB,GAAG,GAAG,CAAA;AAEjC,UAAU,0BAA0B;IAClC,QAAQ,EAAE,OAAO,eAAe,CAAA;IAChC,YAAY,EAAE,CAAC,WAAW,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE,WAAW,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,WAAW,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAA;CAChC;AAID,MAAM,WAAW,4BAA6B,SAAQ,iBAAiB;IACrE,OAAO,CAAC,EAAE,sBAAsB,CAAA;CACjC;AAED,eAAO,MAAM,uBAAuB,EAAE,KAAK,CAAC,EAAE,CAAC,4BAA4B,CAqE1E,CAAA;AAED,eAAO,MAAM,kBAAkB,QAAO,0BAMrC,CAAA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useCallback } from 'react';
|
|
3
|
+
import SessionRecorder, { SessionType } from '@multiplayer-app/session-recorder-browser';
|
|
4
|
+
import { sessionRecorderStore } from './SessionRecorderStore';
|
|
5
|
+
const SessionRecorderContext = createContext(null);
|
|
6
|
+
export const SessionRecorderProvider = ({ children, options }) => {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (options) {
|
|
9
|
+
SessionRecorder.init(options);
|
|
10
|
+
}
|
|
11
|
+
sessionRecorderStore.setState({ isInitialized: SessionRecorder.isInitialized });
|
|
12
|
+
}, []);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
sessionRecorderStore.setState({
|
|
15
|
+
sessionState: SessionRecorder.sessionState,
|
|
16
|
+
sessionType: SessionRecorder.sessionType
|
|
17
|
+
});
|
|
18
|
+
const onStateChange = (sessionState) => {
|
|
19
|
+
sessionRecorderStore.setState({ sessionState });
|
|
20
|
+
};
|
|
21
|
+
const onInit = () => {
|
|
22
|
+
sessionRecorderStore.setState({ isInitialized: true });
|
|
23
|
+
};
|
|
24
|
+
SessionRecorder.on('state-change', onStateChange);
|
|
25
|
+
SessionRecorder.on('init', onInit);
|
|
26
|
+
return () => {
|
|
27
|
+
SessionRecorder.off('state-change', onStateChange);
|
|
28
|
+
SessionRecorder.off('init', onInit);
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
const startSession = useCallback((sessionType = SessionType.MANUAL) => {
|
|
32
|
+
return SessionRecorder.start(sessionType);
|
|
33
|
+
}, []);
|
|
34
|
+
const stopSession = useCallback((comment) => {
|
|
35
|
+
return SessionRecorder.stop(comment);
|
|
36
|
+
}, []);
|
|
37
|
+
const pauseSession = useCallback(() => {
|
|
38
|
+
return SessionRecorder.pause();
|
|
39
|
+
}, []);
|
|
40
|
+
const resumeSession = useCallback(() => {
|
|
41
|
+
return SessionRecorder.resume();
|
|
42
|
+
}, []);
|
|
43
|
+
const cancelSession = useCallback(() => {
|
|
44
|
+
return SessionRecorder.cancel();
|
|
45
|
+
}, []);
|
|
46
|
+
const saveSession = useCallback(() => {
|
|
47
|
+
return SessionRecorder.save();
|
|
48
|
+
}, []);
|
|
49
|
+
return (_jsx(SessionRecorderContext.Provider, { value: {
|
|
50
|
+
instance: SessionRecorder,
|
|
51
|
+
startSession,
|
|
52
|
+
stopSession,
|
|
53
|
+
pauseSession,
|
|
54
|
+
resumeSession,
|
|
55
|
+
cancelSession,
|
|
56
|
+
saveSession
|
|
57
|
+
}, children: children }));
|
|
58
|
+
};
|
|
59
|
+
export const useSessionRecorder = () => {
|
|
60
|
+
const context = useContext(SessionRecorderContext);
|
|
61
|
+
if (!context) {
|
|
62
|
+
throw new Error('useSessionRecorder must be used within a SessionRecorderProvider');
|
|
63
|
+
}
|
|
64
|
+
return context;
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=SessionRecorderContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionRecorderContext.js","sourceRoot":"","sources":["../../src/context/SessionRecorderContext.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAA0B,MAAM,OAAO,CAAA;AACxG,OAAO,eAAe,EAAE,EAAgB,WAAW,EAAE,MAAM,2CAA2C,CAAA;AACtG,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAc7D,MAAM,sBAAsB,GAAG,aAAa,CAAoC,IAAI,CAAC,CAAA;AAMrF,MAAM,CAAC,MAAM,uBAAuB,GAA2C,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE;IACvG,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,EAAE,CAAC;YACZ,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC/B,CAAC;QACD,oBAAoB,CAAC,QAAQ,CAAC,EAAE,aAAa,EAAE,eAAe,CAAC,aAAa,EAAE,CAAC,CAAA;IACjF,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,SAAS,CAAC,GAAG,EAAE;QACb,oBAAoB,CAAC,QAAQ,CAAC;YAC5B,YAAY,EAAE,eAAe,CAAC,YAAY;YAC1C,WAAW,EAAE,eAAe,CAAC,WAAW;SACzC,CAAC,CAAA;QAEF,MAAM,aAAa,GAAG,CAAC,YAA0B,EAAE,EAAE;YACnD,oBAAoB,CAAC,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;QACjD,CAAC,CAAA;QACD,MAAM,MAAM,GAAG,GAAG,EAAE;YAClB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;QACxD,CAAC,CAAA;QAED,eAAe,CAAC,EAAE,CAAC,cAAc,EAAE,aAAa,CAAC,CAAA;QACjD,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAClC,OAAO,GAAG,EAAE;YACV,eAAe,CAAC,GAAG,CAAC,cAAc,EAAE,aAAa,CAAC,CAAA;YAClD,eAAe,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACrC,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,cAA2B,WAAW,CAAC,MAAM,EAAE,EAAE;QACjF,OAAO,eAAe,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IAC3C,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,WAAW,GAAG,WAAW,CAAC,CAAC,OAAgB,EAAE,EAAE;QACnD,OAAO,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACtC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;QACpC,OAAO,eAAe,CAAC,KAAK,EAAE,CAAA;IAChC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;QACrC,OAAO,eAAe,CAAC,MAAM,EAAE,CAAA;IACjC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;QACrC,OAAO,eAAe,CAAC,MAAM,EAAE,CAAA;IACjC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,OAAO,eAAe,CAAC,IAAI,EAAE,CAAA;IAC/B,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CACL,KAAC,sBAAsB,CAAC,QAAQ,IAC9B,KAAK,EAAE;YACL,QAAQ,EAAE,eAAe;YACzB,YAAY;YACZ,WAAW;YACX,YAAY;YACZ,aAAa;YACb,aAAa;YACb,WAAW;SACZ,YAEA,QAAQ,GAEuB,CACnC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAA+B,EAAE;IACjE,MAAM,OAAO,GAAG,UAAU,CAAC,sBAAsB,CAAC,CAAA;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAA;IACrF,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Store } from './createStore';
|
|
2
|
+
import { SessionState, SessionType } from '@multiplayer-app/session-recorder-browser';
|
|
3
|
+
export type SessionRecorderState = {
|
|
4
|
+
isInitialized: boolean;
|
|
5
|
+
sessionType: SessionType | null;
|
|
6
|
+
sessionState: SessionState | null;
|
|
7
|
+
isOnline: boolean;
|
|
8
|
+
error: string | null;
|
|
9
|
+
};
|
|
10
|
+
export declare const sessionRecorderStore: Store<SessionRecorderState>;
|
|
11
|
+
//# sourceMappingURL=SessionRecorderStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionRecorderStore.d.ts","sourceRoot":"","sources":["../../src/context/SessionRecorderStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAC;AAGtF,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,oBAAoB,CAOzD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionRecorderStore.js","sourceRoot":"","sources":["../../src/context/SessionRecorderStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,eAAe,CAAC;AAYxD,MAAM,CAAC,MAAM,oBAAoB,GAC/B,WAAW,CAAuB;IAChC,aAAa,EAAE,KAAK;IACpB,WAAW,EAAE,IAAI;IACjB,YAAY,EAAE,IAAI;IAClB,QAAQ,EAAE,IAAI;IACd,KAAK,EAAE,IAAI;CACZ,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SessionRecorderState = {
|
|
2
|
+
isInitialized: boolean;
|
|
3
|
+
sessionType: any | null;
|
|
4
|
+
sessionState: any | null;
|
|
5
|
+
isOnline: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
};
|
|
8
|
+
type Listener<T> = (state: T, prev: T) => void;
|
|
9
|
+
export type Store<T> = {
|
|
10
|
+
getState: () => T;
|
|
11
|
+
setState: (partial: Partial<T> | ((prev: T) => T)) => void;
|
|
12
|
+
subscribe: (listener: Listener<T>) => () => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function createStore<T extends object>(initialState: T): Store<T>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=createStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createStore.d.ts","sourceRoot":"","sources":["../../src/context/createStore.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,OAAO,CAAA;IACtB,WAAW,EAAE,GAAG,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,GAAG,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAED,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;AAE9C,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IACrB,QAAQ,EAAE,MAAM,CAAC,CAAA;IACjB,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAA;IAC1D,SAAS,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,CAAA;CACjD,CAAA;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAoBvE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createStore(initialState) {
|
|
2
|
+
let state = initialState;
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
const getState = () => state;
|
|
5
|
+
const setState = (partial) => {
|
|
6
|
+
const prev = state;
|
|
7
|
+
const next = typeof partial === 'function' ? partial(prev) : { ...prev, ...partial };
|
|
8
|
+
if (Object.is(next, prev))
|
|
9
|
+
return;
|
|
10
|
+
state = next;
|
|
11
|
+
listeners.forEach((l) => l(state, prev));
|
|
12
|
+
};
|
|
13
|
+
const subscribe = (listener) => {
|
|
14
|
+
listeners.add(listener);
|
|
15
|
+
return () => listeners.delete(listener);
|
|
16
|
+
};
|
|
17
|
+
return { getState, setState, subscribe };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=createStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createStore.js","sourceRoot":"","sources":["../../src/context/createStore.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,WAAW,CAAmB,YAAe;IAC3D,IAAI,KAAK,GAAG,YAAY,CAAA;IACxB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAe,CAAA;IAExC,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,KAAK,CAAA;IAE5B,MAAM,QAAQ,GAAyB,CAAC,OAAO,EAAE,EAAE;QACjD,MAAM,IAAI,GAAG,KAAK,CAAA;QAClB,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,UAAU,CAAC,CAAC,CAAE,OAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,EAAQ,CAAA;QAC5G,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC;YAAE,OAAM;QACjC,KAAK,GAAG,IAAI,CAAA;QACZ,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAA;IAED,MAAM,SAAS,GAA0B,CAAC,QAAQ,EAAE,EAAE;QACpD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACvB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACzC,CAAC,CAAA;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAA;AAC1C,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SessionState, SessionType } from '@multiplayer-app/session-recorder-browser';
|
|
2
|
+
import { type SessionRecorderState } from './SessionRecorderStore';
|
|
3
|
+
/**
|
|
4
|
+
* Select a derived slice from the shared Session Recorder store.
|
|
5
|
+
* Works in both React (web) and React Native since the store shape is identical.
|
|
6
|
+
*
|
|
7
|
+
* @param selector - Function that maps the full store state to the slice you need
|
|
8
|
+
* @param equalityFn - Optional comparator to avoid unnecessary re-renders
|
|
9
|
+
* @returns The selected slice of state
|
|
10
|
+
*/
|
|
11
|
+
export declare function useSessionRecorderStore<TSlice>(selector: (s: SessionRecorderState) => TSlice, equalityFn?: (a: TSlice, b: TSlice) => boolean): TSlice;
|
|
12
|
+
/**
|
|
13
|
+
* Read the current session recording state (started, paused, stopped).
|
|
14
|
+
*/
|
|
15
|
+
export declare function useSessionRecordingState(): SessionState | null;
|
|
16
|
+
/**
|
|
17
|
+
* Read the current session type (MANUAL/CONTINUOUS).
|
|
18
|
+
*/
|
|
19
|
+
export declare function useSessionType(): SessionType | null;
|
|
20
|
+
/**
|
|
21
|
+
* Check whether the Session Recorder has been initialized.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useIsInitialized(): boolean;
|
|
24
|
+
//# sourceMappingURL=useSessionRecorderStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSessionRecorderStore.d.ts","sourceRoot":"","sources":["../../src/context/useSessionRecorderStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAC5C,QAAQ,EAAE,CAAC,CAAC,EAAE,oBAAoB,KAAK,MAAM,EAC7C,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,GAC7C,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,wBAAwB,wBAEvC;AAED;;GAEG;AACH,wBAAgB,cAAc,uBAE7B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,YAE/B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { sessionRecorderStore, } from './SessionRecorderStore';
|
|
2
|
+
import { useStoreSelector } from './useStoreSelector';
|
|
3
|
+
/**
|
|
4
|
+
* Select a derived slice from the shared Session Recorder store.
|
|
5
|
+
* Works in both React (web) and React Native since the store shape is identical.
|
|
6
|
+
*
|
|
7
|
+
* @param selector - Function that maps the full store state to the slice you need
|
|
8
|
+
* @param equalityFn - Optional comparator to avoid unnecessary re-renders
|
|
9
|
+
* @returns The selected slice of state
|
|
10
|
+
*/
|
|
11
|
+
export function useSessionRecorderStore(selector, equalityFn) {
|
|
12
|
+
return useStoreSelector(sessionRecorderStore, selector, equalityFn);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read the current session recording state (started, paused, stopped).
|
|
16
|
+
*/
|
|
17
|
+
export function useSessionRecordingState() {
|
|
18
|
+
return useSessionRecorderStore((s) => s.sessionState);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read the current session type (MANUAL/CONTINUOUS).
|
|
22
|
+
*/
|
|
23
|
+
export function useSessionType() {
|
|
24
|
+
return useSessionRecorderStore((s) => s.sessionType);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check whether the Session Recorder has been initialized.
|
|
28
|
+
*/
|
|
29
|
+
export function useIsInitialized() {
|
|
30
|
+
return useSessionRecorderStore((s) => s.isInitialized);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=useSessionRecorderStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSessionRecorderStore.js","sourceRoot":"","sources":["../../src/context/useSessionRecorderStore.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CACrC,QAA6C,EAC7C,UAA8C;IAE9C,OAAO,gBAAgB,CACrB,oBAAoB,EACpB,QAAQ,EACR,UAAU,CACX,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,uBAAuB,CAAsB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;AAC7E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,uBAAuB,CAAqB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;AAC3E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,uBAAuB,CAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Store } from './createStore';
|
|
2
|
+
import { shallowEqual } from '../utils/shallowEqual';
|
|
3
|
+
export declare function useStoreSelector<TState extends object, TSlice>(store: Store<TState>, selector: (state: TState) => TSlice, equalityFn?: (a: TSlice, b: TSlice) => boolean): TSlice;
|
|
4
|
+
export declare const shallow: typeof shallowEqual;
|
|
5
|
+
//# sourceMappingURL=useStoreSelector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStoreSelector.d.ts","sourceRoot":"","sources":["../../src/context/useStoreSelector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,wBAAgB,gBAAgB,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAC5D,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,EACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EACnC,UAAU,GAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAmB,GACxD,MAAM,CAyBR;AAED,eAAO,MAAM,OAAO,qBAAe,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { shallowEqual } from '../utils/shallowEqual';
|
|
3
|
+
export function useStoreSelector(store, selector, equalityFn = Object.is) {
|
|
4
|
+
const latestSelectorRef = useRef(selector);
|
|
5
|
+
const latestEqualityRef = useRef(equalityFn);
|
|
6
|
+
latestSelectorRef.current = selector;
|
|
7
|
+
latestEqualityRef.current = equalityFn;
|
|
8
|
+
const [slice, setSlice] = useState(() => latestSelectorRef.current(store.getState()));
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
function handleChange(nextState, prevState) {
|
|
11
|
+
const nextSlice = latestSelectorRef.current(nextState);
|
|
12
|
+
const prevSlice = latestSelectorRef.current(prevState);
|
|
13
|
+
if (!latestEqualityRef.current(nextSlice, prevSlice)) {
|
|
14
|
+
setSlice(nextSlice);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const unsubscribe = store.subscribe(handleChange);
|
|
18
|
+
// Sync once in case changed between render and effect
|
|
19
|
+
handleChange(store.getState(), store.getState());
|
|
20
|
+
return unsubscribe;
|
|
21
|
+
}, [store]);
|
|
22
|
+
return slice;
|
|
23
|
+
}
|
|
24
|
+
export const shallow = shallowEqual;
|
|
25
|
+
//# sourceMappingURL=useStoreSelector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStoreSelector.js","sourceRoot":"","sources":["../../src/context/useStoreSelector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,UAAU,gBAAgB,CAC9B,KAAoB,EACpB,QAAmC,EACnC,aAAgD,MAAM,CAAC,EAAE;IAEzD,MAAM,iBAAiB,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,iBAAiB,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IAC7C,iBAAiB,CAAC,OAAO,GAAG,QAAQ,CAAC;IACrC,iBAAiB,CAAC,OAAO,GAAG,UAAU,CAAC;IAEvC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAS,GAAG,EAAE,CAC9C,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAC5C,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,YAAY,CAAC,SAAiB,EAAE,SAAiB;YACxD,MAAM,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACvD,MAAM,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACvD,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;gBACrD,QAAQ,CAAC,SAAS,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QAClD,sDAAsD;QACtD,YAAY,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QACjD,OAAO,WAAW,CAAC;IACrB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,YAAY,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import SessionRecorderBrowser from '@multiplayer-app/session-recorder-browser';
|
|
2
|
+
export * from '@multiplayer-app/session-recorder-browser';
|
|
3
|
+
export * from './context/SessionRecorderContext';
|
|
4
|
+
export * from './context/useSessionRecorderStore';
|
|
5
|
+
export { useNavigationRecorder } from './navigation';
|
|
6
|
+
export type { UseNavigationRecorderOptions } from './navigation';
|
|
7
|
+
export default SessionRecorderBrowser;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,MAAM,2CAA2C,CAAC;AAE/E,cAAc,2CAA2C,CAAC;AAC1D,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAElD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACpD,YAAY,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAA;AAEhE,eAAe,sBAAsB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import SessionRecorderBrowser from '@multiplayer-app/session-recorder-browser';
|
|
2
|
+
export * from '@multiplayer-app/session-recorder-browser';
|
|
3
|
+
export * from './context/SessionRecorderContext';
|
|
4
|
+
export * from './context/useSessionRecorderStore';
|
|
5
|
+
export { useNavigationRecorder } from './navigation';
|
|
6
|
+
export default SessionRecorderBrowser;
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,MAAM,2CAA2C,CAAC;AAE/E,cAAc,2CAA2C,CAAC;AAC1D,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAElD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAGpD,eAAe,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NavigationSignal } from '@multiplayer-app/session-recorder-browser';
|
|
2
|
+
export interface UseNavigationRecorderOptions extends Partial<Omit<NavigationSignal, 'path' | 'timestamp'>> {
|
|
3
|
+
/**
|
|
4
|
+
* Overrides the path sent to the recorder. Defaults to the provided pathname argument.
|
|
5
|
+
*/
|
|
6
|
+
path?: string;
|
|
7
|
+
/**
|
|
8
|
+
* When true (default), document.title is captured if available.
|
|
9
|
+
*/
|
|
10
|
+
captureDocumentTitle?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* React Router compatible navigation recorder hook.
|
|
14
|
+
* Call inside a component where you can access current location and navigation events.
|
|
15
|
+
* Example:
|
|
16
|
+
* const location = useLocation();
|
|
17
|
+
* useNavigationRecorder(location.pathname);
|
|
18
|
+
*/
|
|
19
|
+
export declare function useNavigationRecorder(pathname: string, options?: UseNavigationRecorderOptions): void;
|
|
20
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AACA,OAA+B,EAC7B,gBAAgB,EACjB,MAAM,2CAA2C,CAAA;AAElD,MAAM,WAAW,4BACf,SAAQ,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC;IAC7D;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,4BAA4B,GACrC,IAAI,CA0DN"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import SessionRecorderBrowser from '@multiplayer-app/session-recorder-browser';
|
|
3
|
+
/**
|
|
4
|
+
* React Router compatible navigation recorder hook.
|
|
5
|
+
* Call inside a component where you can access current location and navigation events.
|
|
6
|
+
* Example:
|
|
7
|
+
* const location = useLocation();
|
|
8
|
+
* useNavigationRecorder(location.pathname);
|
|
9
|
+
*/
|
|
10
|
+
export function useNavigationRecorder(pathname, options) {
|
|
11
|
+
const optionsRef = useRef(options);
|
|
12
|
+
const hasRecordedInitialRef = useRef(false);
|
|
13
|
+
const lastPathRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
optionsRef.current = options;
|
|
16
|
+
}, [options]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!pathname || !SessionRecorderBrowser?.navigation) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const resolvedOptions = optionsRef.current || {};
|
|
22
|
+
const resolvedPath = resolvedOptions.path ?? pathname;
|
|
23
|
+
if (!resolvedPath) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (lastPathRef.current === resolvedPath && !resolvedOptions.navigationType) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const captureDocumentTitle = resolvedOptions.captureDocumentTitle ?? true;
|
|
30
|
+
const signal = {
|
|
31
|
+
path: resolvedPath,
|
|
32
|
+
routeName: resolvedOptions.routeName ?? resolvedPath,
|
|
33
|
+
title: resolvedOptions.title ??
|
|
34
|
+
(captureDocumentTitle && typeof document !== 'undefined'
|
|
35
|
+
? document.title
|
|
36
|
+
: undefined),
|
|
37
|
+
url: resolvedOptions.url,
|
|
38
|
+
params: resolvedOptions.params,
|
|
39
|
+
state: resolvedOptions.state,
|
|
40
|
+
navigationType: resolvedOptions.navigationType ??
|
|
41
|
+
(hasRecordedInitialRef.current ? undefined : 'initial'),
|
|
42
|
+
framework: resolvedOptions.framework ?? 'react',
|
|
43
|
+
source: resolvedOptions.source ?? 'react-router',
|
|
44
|
+
metadata: resolvedOptions.metadata,
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
SessionRecorderBrowser.navigation.record(signal);
|
|
48
|
+
hasRecordedInitialRef.current = true;
|
|
49
|
+
lastPathRef.current = resolvedPath;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.warn('[SessionRecorder][React] Failed to record navigation', error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, [pathname]);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,sBAEN,MAAM,2CAA2C,CAAA;AAclD;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAgB,EAChB,OAAsC;IAEtC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;IAClC,MAAM,qBAAqB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,WAAW,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAA;IAE/C,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,CAAC,OAAO,GAAG,OAAO,CAAA;IAC9B,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAEb,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,QAAQ,IAAI,CAAC,sBAAsB,EAAE,UAAU,EAAE,CAAC;YACrD,OAAM;QACR,CAAC;QAED,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,IAAI,EAAE,CAAA;QAChD,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,IAAI,QAAQ,CAAA;QAErD,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAM;QACR,CAAC;QAED,IAAI,WAAW,CAAC,OAAO,KAAK,YAAY,IAAI,CAAC,eAAe,CAAC,cAAc,EAAE,CAAC;YAC5E,OAAM;QACR,CAAC;QAED,MAAM,oBAAoB,GACxB,eAAe,CAAC,oBAAoB,IAAI,IAAI,CAAA;QAE9C,MAAM,MAAM,GAAqB;YAC/B,IAAI,EAAE,YAAY;YAClB,SAAS,EAAE,eAAe,CAAC,SAAS,IAAI,YAAY;YACpD,KAAK,EACH,eAAe,CAAC,KAAK;gBACrB,CAAC,oBAAoB,IAAI,OAAO,QAAQ,KAAK,WAAW;oBACtD,CAAC,CAAC,QAAQ,CAAC,KAAK;oBAChB,CAAC,CAAC,SAAS,CAAC;YAChB,GAAG,EAAE,eAAe,CAAC,GAAG;YACxB,MAAM,EAAE,eAAe,CAAC,MAAM;YAC9B,KAAK,EAAE,eAAe,CAAC,KAAK;YAC5B,cAAc,EACZ,eAAe,CAAC,cAAc;gBAC9B,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YACzD,SAAS,EAAE,eAAe,CAAC,SAAS,IAAI,OAAO;YAC/C,MAAM,EAAE,eAAe,CAAC,MAAM,IAAI,cAAc;YAChD,QAAQ,EAAE,eAAe,CAAC,QAAQ;SACnC,CAAA;QAED,IAAI,CAAC;YACH,sBAAsB,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAChD,qBAAqB,CAAC,OAAO,GAAG,IAAI,CAAA;YACpC,WAAW,CAAC,OAAO,GAAG,YAAY,CAAA;QACpC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;gBAC1C,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CAAC,sDAAsD,EAAE,KAAK,CAAC,CAAA;YAC7E,CAAC;QACH,CAAC;IACH,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shallowEqual.d.ts","sourceRoot":"","sources":["../../src/utils/shallowEqual.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACxD,CAAC,EAAE,CAAC,EACJ,CAAC,EAAE,CAAC,GACH,OAAO,CAgBT"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function shallowEqual(a, b) {
|
|
2
|
+
if (Object.is(a, b))
|
|
3
|
+
return true;
|
|
4
|
+
if (!a || !b)
|
|
5
|
+
return false;
|
|
6
|
+
const aKeys = Object.keys(a);
|
|
7
|
+
const bKeys = Object.keys(b);
|
|
8
|
+
if (aKeys.length !== bKeys.length)
|
|
9
|
+
return false;
|
|
10
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
11
|
+
const key = aKeys[i];
|
|
12
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) ||
|
|
13
|
+
!Object.is(a[key], b[key])) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=shallowEqual.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shallowEqual.js","sourceRoot":"","sources":["../../src/utils/shallowEqual.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY,CAC1B,CAAI,EACJ,CAAI;IAEJ,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,IACE,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,GAAI,CAAC;YAC9C,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,GAAI,CAAC,EAAE,CAAC,CAAC,GAAI,CAAC,CAAC,EAC5B,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@multiplayer-app/session-recorder-react",
|
|
3
|
+
"version": "1.2.15",
|
|
4
|
+
"description": "Multiplayer Fullstack Session Recorder for React (browser wrapper)",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Multiplayer Software, Inc.",
|
|
7
|
+
"url": "https://www.multiplayer.app"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/multiplayer-app/multiplayer-session-recorder-javascript.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript/issues"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"scripts": {
|
|
28
|
+
"lint": "eslint src/**/*.{ts,tsx}",
|
|
29
|
+
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc --build tsconfig.json",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": ">=17",
|
|
34
|
+
"react-dom": ">=17",
|
|
35
|
+
"@opentelemetry/api": "^1.9.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@multiplayer-app/session-recorder-browser": "1.2.15",
|
|
39
|
+
"@multiplayer-app/session-recorder-common": "1.2.15"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"eslint": "8.48.0",
|
|
43
|
+
"typescript": "5.7.3",
|
|
44
|
+
"@types/react": "^18.2.66"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import SessionRecorderBrowser from '@multiplayer-app/session-recorder-browser';
|
|
2
|
+
|
|
3
|
+
export * from '@multiplayer-app/session-recorder-browser';
|
|
4
|
+
export * from './context/SessionRecorderContext';
|
|
5
|
+
export * from './context/useSessionRecorderStore';
|
|
6
|
+
|
|
7
|
+
export { useNavigationRecorder } from './navigation'
|
|
8
|
+
export type { UseNavigationRecorderOptions } from './navigation'
|
|
9
|
+
|
|
10
|
+
export default SessionRecorderBrowser
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|