@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.
Files changed (44) hide show
  1. package/README.md +386 -0
  2. package/dist/context/SessionRecorderContext.d.ts +19 -0
  3. package/dist/context/SessionRecorderContext.d.ts.map +1 -0
  4. package/dist/context/SessionRecorderContext.js +66 -0
  5. package/dist/context/SessionRecorderContext.js.map +1 -0
  6. package/dist/context/SessionRecorderStore.d.ts +11 -0
  7. package/dist/context/SessionRecorderStore.d.ts.map +1 -0
  8. package/dist/context/SessionRecorderStore.js +9 -0
  9. package/dist/context/SessionRecorderStore.js.map +1 -0
  10. package/dist/context/createStore.d.ts +16 -0
  11. package/dist/context/createStore.d.ts.map +1 -0
  12. package/dist/context/createStore.js +19 -0
  13. package/dist/context/createStore.js.map +1 -0
  14. package/dist/context/useSessionRecorderStore.d.ts +24 -0
  15. package/dist/context/useSessionRecorderStore.d.ts.map +1 -0
  16. package/dist/context/useSessionRecorderStore.js +32 -0
  17. package/dist/context/useSessionRecorderStore.js.map +1 -0
  18. package/dist/context/useStoreSelector.d.ts +5 -0
  19. package/dist/context/useStoreSelector.d.ts.map +1 -0
  20. package/dist/context/useStoreSelector.js +25 -0
  21. package/dist/context/useStoreSelector.js.map +1 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/navigation.d.ts +20 -0
  27. package/dist/navigation.d.ts.map +1 -0
  28. package/dist/navigation.js +59 -0
  29. package/dist/navigation.js.map +1 -0
  30. package/dist/utils/shallowEqual.d.ts +2 -0
  31. package/dist/utils/shallowEqual.d.ts.map +1 -0
  32. package/dist/utils/shallowEqual.js +19 -0
  33. package/dist/utils/shallowEqual.js.map +1 -0
  34. package/docs/img/header-js.png +0 -0
  35. package/package.json +46 -0
  36. package/src/context/SessionRecorderContext.tsx +100 -0
  37. package/src/context/SessionRecorderStore.ts +20 -0
  38. package/src/context/createStore.ts +37 -0
  39. package/src/context/useSessionRecorderStore.ts +47 -0
  40. package/src/context/useStoreSelector.ts +36 -0
  41. package/src/index.ts +10 -0
  42. package/src/navigation.ts +86 -0
  43. package/src/utils/shallowEqual.ts +20 -0
  44. package/tsconfig.json +19 -0
package/README.md ADDED
@@ -0,0 +1,386 @@
1
+ ![Description](./docs/img/header-js.png)
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,9 @@
1
+ import { createStore } from './createStore';
2
+ export const sessionRecorderStore = createStore({
3
+ isInitialized: false,
4
+ sessionType: null,
5
+ sessionState: null,
6
+ isOnline: true,
7
+ error: null,
8
+ });
9
+ //# sourceMappingURL=SessionRecorderStore.js.map
@@ -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"}
@@ -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,2 @@
1
+ export declare function shallowEqual<T extends Record<string, any>>(a: T, b: T): boolean;
2
+ //# sourceMappingURL=shallowEqual.d.ts.map
@@ -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
+ }