@neroom/nevision 0.1.0

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 ADDED
@@ -0,0 +1,71 @@
1
+ # ne-room-react
2
+
3
+ React SDK for Ne-Room session recording.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install ne-room-react
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Add the `NeRoomRecorder` component to your app's root layout:
14
+
15
+ ```tsx
16
+ import { NeRoomRecorder } from 'ne-room-react';
17
+
18
+ export default function RootLayout({ children }) {
19
+ return (
20
+ <html>
21
+ <body>
22
+ <NeRoomRecorder
23
+ siteId="your-site-id"
24
+ apiKey="your-api-key"
25
+ />
26
+ {children}
27
+ </body>
28
+ </html>
29
+ );
30
+ }
31
+ ```
32
+
33
+ ## Props
34
+
35
+ | Prop | Type | Required | Description |
36
+ |------|------|----------|-------------|
37
+ | `siteId` | `string` | Yes | Your Ne-Room site ID |
38
+ | `apiKey` | `string` | Yes | Your Ne-Room API key |
39
+ | `apiUrl` | `string` | No | Custom API URL (default: `https://api.ne-room.io`) |
40
+ | `sampling` | `object` | No | Sampling options for performance |
41
+ | `privacy` | `object` | No | Privacy masking options |
42
+ | `onStart` | `(sessionId: string) => void` | No | Callback when recording starts |
43
+ | `onError` | `(error: Error) => void` | No | Callback on error |
44
+
45
+ ## Privacy Masking
46
+
47
+ By default, password and email inputs are masked. You can customize this:
48
+
49
+ ```tsx
50
+ <NeRoomRecorder
51
+ siteId="your-site-id"
52
+ apiKey="your-api-key"
53
+ privacy={{
54
+ maskAllInputs: true,
55
+ maskTextSelector: '.sensitive-data',
56
+ blockSelector: '.do-not-record',
57
+ }}
58
+ />
59
+ ```
60
+
61
+ ### CSS Classes
62
+
63
+ Add these classes to elements for privacy control:
64
+
65
+ - `.rr-mask` - Masks text content (shows asterisks)
66
+ - `.rr-block` - Blocks the entire element from recording
67
+ - `.rr-ignore` - Ignores changes to this element
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,32 @@
1
+ interface NeRoomRecorderProps {
2
+ siteId: string;
3
+ apiKey: string;
4
+ apiUrl?: string;
5
+ /** Sampling options for performance */
6
+ sampling?: {
7
+ mousemove?: boolean | number;
8
+ mouseInteraction?: boolean;
9
+ scroll?: number;
10
+ media?: number;
11
+ input?: "all" | "last";
12
+ };
13
+ /** Privacy masking options */
14
+ privacy?: {
15
+ maskAllInputs?: boolean;
16
+ maskInputOptions?: {
17
+ password?: boolean;
18
+ email?: boolean;
19
+ text?: boolean;
20
+ tel?: boolean;
21
+ };
22
+ maskTextSelector?: string;
23
+ blockSelector?: string;
24
+ };
25
+ /** Callback when recording starts */
26
+ onStart?: (sessionId: string) => void;
27
+ /** Callback on error */
28
+ onError?: (error: Error) => void;
29
+ }
30
+ declare function NeRoomRecorder({ siteId, apiKey, apiUrl, sampling, privacy, onStart, onError, }: NeRoomRecorderProps): null;
31
+
32
+ export { NeRoomRecorder, type NeRoomRecorderProps, NeRoomRecorder as default };
@@ -0,0 +1,32 @@
1
+ interface NeRoomRecorderProps {
2
+ siteId: string;
3
+ apiKey: string;
4
+ apiUrl?: string;
5
+ /** Sampling options for performance */
6
+ sampling?: {
7
+ mousemove?: boolean | number;
8
+ mouseInteraction?: boolean;
9
+ scroll?: number;
10
+ media?: number;
11
+ input?: "all" | "last";
12
+ };
13
+ /** Privacy masking options */
14
+ privacy?: {
15
+ maskAllInputs?: boolean;
16
+ maskInputOptions?: {
17
+ password?: boolean;
18
+ email?: boolean;
19
+ text?: boolean;
20
+ tel?: boolean;
21
+ };
22
+ maskTextSelector?: string;
23
+ blockSelector?: string;
24
+ };
25
+ /** Callback when recording starts */
26
+ onStart?: (sessionId: string) => void;
27
+ /** Callback on error */
28
+ onError?: (error: Error) => void;
29
+ }
30
+ declare function NeRoomRecorder({ siteId, apiKey, apiUrl, sampling, privacy, onStart, onError, }: NeRoomRecorderProps): null;
31
+
32
+ export { NeRoomRecorder, type NeRoomRecorderProps, NeRoomRecorder as default };
package/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.tsx
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ NeRoomRecorder: () => NeRoomRecorder,
35
+ default: () => index_default
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+ var import_react = require("react");
39
+ var DEFAULT_API_URL = "https://api.ne-room.io";
40
+ var CHUNK_INTERVAL = 1e4;
41
+ var MAX_EVENTS_PER_CHUNK = 100;
42
+ function NeRoomRecorder({
43
+ siteId,
44
+ apiKey,
45
+ apiUrl = DEFAULT_API_URL,
46
+ sampling,
47
+ privacy,
48
+ onStart,
49
+ onError
50
+ }) {
51
+ const sessionIdRef = (0, import_react.useRef)(null);
52
+ const eventsBufferRef = (0, import_react.useRef)([]);
53
+ const stopFnRef = (0, import_react.useRef)(null);
54
+ const chunkIndexRef = (0, import_react.useRef)(0);
55
+ (0, import_react.useEffect)(() => {
56
+ let intervalId;
57
+ const init = async () => {
58
+ try {
59
+ const { record } = await import("rrweb");
60
+ const startResponse = await fetch(`${apiUrl}/public/recordings/start`, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json"
64
+ },
65
+ body: JSON.stringify({
66
+ siteId,
67
+ apiKey,
68
+ url: window.location.href,
69
+ userAgent: navigator.userAgent,
70
+ screenWidth: window.screen.width,
71
+ screenHeight: window.screen.height,
72
+ viewportWidth: window.innerWidth,
73
+ viewportHeight: window.innerHeight
74
+ })
75
+ });
76
+ if (!startResponse.ok) {
77
+ throw new Error(`Failed to start recording: ${startResponse.status}`);
78
+ }
79
+ const { sessionId } = await startResponse.json();
80
+ sessionIdRef.current = sessionId;
81
+ onStart?.(sessionId);
82
+ const recordConfig = {
83
+ emit: (event) => {
84
+ eventsBufferRef.current.push(event);
85
+ },
86
+ sampling: sampling ? {
87
+ mousemove: sampling.mousemove,
88
+ mouseInteraction: sampling.mouseInteraction,
89
+ scroll: sampling.scroll,
90
+ media: sampling.media,
91
+ input: sampling.input
92
+ } : {
93
+ mousemove: 50,
94
+ scroll: 150
95
+ },
96
+ maskAllInputs: privacy?.maskAllInputs ?? true,
97
+ maskInputOptions: privacy?.maskInputOptions ?? {
98
+ password: true,
99
+ email: true
100
+ },
101
+ maskTextSelector: privacy?.maskTextSelector,
102
+ blockSelector: privacy?.blockSelector
103
+ };
104
+ const stopFn = record(recordConfig);
105
+ if (stopFn) {
106
+ stopFnRef.current = stopFn;
107
+ }
108
+ intervalId = setInterval(() => {
109
+ sendChunk(apiUrl, sessionId);
110
+ }, CHUNK_INTERVAL);
111
+ const handleUnload = () => {
112
+ if (sessionIdRef.current && eventsBufferRef.current.length > 0) {
113
+ sendChunk(apiUrl, sessionIdRef.current, true);
114
+ endSession(apiUrl, sessionIdRef.current);
115
+ }
116
+ };
117
+ window.addEventListener("beforeunload", handleUnload);
118
+ document.addEventListener("visibilitychange", () => {
119
+ if (document.visibilityState === "hidden") {
120
+ handleUnload();
121
+ }
122
+ });
123
+ } catch (error) {
124
+ onError?.(error instanceof Error ? error : new Error(String(error)));
125
+ }
126
+ };
127
+ const sendChunk = async (url, sessionId, isBeacon = false) => {
128
+ if (eventsBufferRef.current.length === 0) return;
129
+ const events = eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
130
+ const chunkIndex = chunkIndexRef.current++;
131
+ const payload = JSON.stringify({
132
+ sessionId,
133
+ siteId,
134
+ apiKey,
135
+ events,
136
+ chunkIndex
137
+ });
138
+ if (isBeacon && navigator.sendBeacon) {
139
+ navigator.sendBeacon(
140
+ `${url}/public/recordings/chunk`,
141
+ new Blob([payload], { type: "application/json" })
142
+ );
143
+ } else {
144
+ try {
145
+ await fetch(`${url}/public/recordings/chunk`, {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: payload
149
+ });
150
+ } catch {
151
+ eventsBufferRef.current.unshift(...events);
152
+ }
153
+ }
154
+ };
155
+ const endSession = (url, sessionId) => {
156
+ navigator.sendBeacon?.(
157
+ `${url}/public/recordings/end`,
158
+ new Blob(
159
+ [JSON.stringify({ sessionId, siteId, apiKey })],
160
+ { type: "application/json" }
161
+ )
162
+ );
163
+ };
164
+ init();
165
+ return () => {
166
+ clearInterval(intervalId);
167
+ stopFnRef.current?.();
168
+ if (sessionIdRef.current) {
169
+ sendChunk(apiUrl, sessionIdRef.current, true);
170
+ endSession(apiUrl, sessionIdRef.current);
171
+ }
172
+ };
173
+ }, [siteId, apiKey, apiUrl, sampling, privacy, onStart, onError]);
174
+ return null;
175
+ }
176
+ var index_default = NeRoomRecorder;
177
+ // Annotate the CommonJS export names for ESM import in node:
178
+ 0 && (module.exports = {
179
+ NeRoomRecorder
180
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ // src/index.tsx
4
+ import { useEffect, useRef } from "react";
5
+ var DEFAULT_API_URL = "https://api.ne-room.io";
6
+ var CHUNK_INTERVAL = 1e4;
7
+ var MAX_EVENTS_PER_CHUNK = 100;
8
+ function NeRoomRecorder({
9
+ siteId,
10
+ apiKey,
11
+ apiUrl = DEFAULT_API_URL,
12
+ sampling,
13
+ privacy,
14
+ onStart,
15
+ onError
16
+ }) {
17
+ const sessionIdRef = useRef(null);
18
+ const eventsBufferRef = useRef([]);
19
+ const stopFnRef = useRef(null);
20
+ const chunkIndexRef = useRef(0);
21
+ useEffect(() => {
22
+ let intervalId;
23
+ const init = async () => {
24
+ try {
25
+ const { record } = await import("rrweb");
26
+ const startResponse = await fetch(`${apiUrl}/public/recordings/start`, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json"
30
+ },
31
+ body: JSON.stringify({
32
+ siteId,
33
+ apiKey,
34
+ url: window.location.href,
35
+ userAgent: navigator.userAgent,
36
+ screenWidth: window.screen.width,
37
+ screenHeight: window.screen.height,
38
+ viewportWidth: window.innerWidth,
39
+ viewportHeight: window.innerHeight
40
+ })
41
+ });
42
+ if (!startResponse.ok) {
43
+ throw new Error(`Failed to start recording: ${startResponse.status}`);
44
+ }
45
+ const { sessionId } = await startResponse.json();
46
+ sessionIdRef.current = sessionId;
47
+ onStart?.(sessionId);
48
+ const recordConfig = {
49
+ emit: (event) => {
50
+ eventsBufferRef.current.push(event);
51
+ },
52
+ sampling: sampling ? {
53
+ mousemove: sampling.mousemove,
54
+ mouseInteraction: sampling.mouseInteraction,
55
+ scroll: sampling.scroll,
56
+ media: sampling.media,
57
+ input: sampling.input
58
+ } : {
59
+ mousemove: 50,
60
+ scroll: 150
61
+ },
62
+ maskAllInputs: privacy?.maskAllInputs ?? true,
63
+ maskInputOptions: privacy?.maskInputOptions ?? {
64
+ password: true,
65
+ email: true
66
+ },
67
+ maskTextSelector: privacy?.maskTextSelector,
68
+ blockSelector: privacy?.blockSelector
69
+ };
70
+ const stopFn = record(recordConfig);
71
+ if (stopFn) {
72
+ stopFnRef.current = stopFn;
73
+ }
74
+ intervalId = setInterval(() => {
75
+ sendChunk(apiUrl, sessionId);
76
+ }, CHUNK_INTERVAL);
77
+ const handleUnload = () => {
78
+ if (sessionIdRef.current && eventsBufferRef.current.length > 0) {
79
+ sendChunk(apiUrl, sessionIdRef.current, true);
80
+ endSession(apiUrl, sessionIdRef.current);
81
+ }
82
+ };
83
+ window.addEventListener("beforeunload", handleUnload);
84
+ document.addEventListener("visibilitychange", () => {
85
+ if (document.visibilityState === "hidden") {
86
+ handleUnload();
87
+ }
88
+ });
89
+ } catch (error) {
90
+ onError?.(error instanceof Error ? error : new Error(String(error)));
91
+ }
92
+ };
93
+ const sendChunk = async (url, sessionId, isBeacon = false) => {
94
+ if (eventsBufferRef.current.length === 0) return;
95
+ const events = eventsBufferRef.current.splice(0, MAX_EVENTS_PER_CHUNK);
96
+ const chunkIndex = chunkIndexRef.current++;
97
+ const payload = JSON.stringify({
98
+ sessionId,
99
+ siteId,
100
+ apiKey,
101
+ events,
102
+ chunkIndex
103
+ });
104
+ if (isBeacon && navigator.sendBeacon) {
105
+ navigator.sendBeacon(
106
+ `${url}/public/recordings/chunk`,
107
+ new Blob([payload], { type: "application/json" })
108
+ );
109
+ } else {
110
+ try {
111
+ await fetch(`${url}/public/recordings/chunk`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: payload
115
+ });
116
+ } catch {
117
+ eventsBufferRef.current.unshift(...events);
118
+ }
119
+ }
120
+ };
121
+ const endSession = (url, sessionId) => {
122
+ navigator.sendBeacon?.(
123
+ `${url}/public/recordings/end`,
124
+ new Blob(
125
+ [JSON.stringify({ sessionId, siteId, apiKey })],
126
+ { type: "application/json" }
127
+ )
128
+ );
129
+ };
130
+ init();
131
+ return () => {
132
+ clearInterval(intervalId);
133
+ stopFnRef.current?.();
134
+ if (sessionIdRef.current) {
135
+ sendChunk(apiUrl, sessionIdRef.current, true);
136
+ endSession(apiUrl, sessionIdRef.current);
137
+ }
138
+ };
139
+ }, [siteId, apiKey, apiUrl, sampling, privacy, onStart, onError]);
140
+ return null;
141
+ }
142
+ var index_default = NeRoomRecorder;
143
+ export {
144
+ NeRoomRecorder,
145
+ index_default as default
146
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@neroom/nevision",
3
+ "version": "0.1.0",
4
+ "description": "React SDK for Neroom session recording",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup src/index.tsx --format cjs,esm --dts --external react",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "peerDependencies": {
16
+ "react": ">=17.0.0"
17
+ },
18
+ "dependencies": {
19
+ "rrweb": "^2.0.0-alpha.11"
20
+ },
21
+ "devDependencies": {
22
+ "@types/react": "^18.2.0",
23
+ "react": "^18.2.0",
24
+ "tsup": "^8.0.0",
25
+ "typescript": "^5.0.0"
26
+ },
27
+ "keywords": [
28
+ "session-recording",
29
+ "rrweb",
30
+ "react",
31
+ "analytics",
32
+ "user-behavior"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/yourusername/nevision-app"
38
+ }
39
+ }