메뉴
HN
Hacker News 56일 전

RePlaya: 셀프 호스팅 방식의 실시간 세션 리플레이 도구

IMP
6/10
핵심 요약

RePlaya는 S2를 기반으로 구축된 셀프 호스팅 세션 리플레이 도구입니다. 별도의 데이터베이스나 저장소 없이 S2 스트림 하나만으로 모든 세션을 기록하고 실시간으로 재생할 수 있는 것이 특징입니다. 사용자가 웹사이트를 탐색하는 동안 실시간으로 세션을 모니터링할 수 있어 웹 서비스 분석 및 디버깅에 매우 유용합니다.

번역된 본문

RePlaya는 S2 기반으로 구축된 셀프 호스팅 세션 리플레이 도구입니다. 각 세션은 하나의 S2 스트림으로 저장되며, 이 스트림이 백엔드 전체를 담당합니다. 별도의 데이터베이스, 메시지 버스, 객체 저장소 또는 검색 인덱스가 필요하지 않습니다. S2 스트림은 기록 중에도 테일링(tailing)할 수 있으므로, RePlaya는 방문자가 여전히 페이지에 있는 동안에도 세션을 실시간으로 리플레이할 수 있으며, 완료된 세션도 재생할 수 있습니다.

웹사이트에 레코더 스니펫을 추가하면 세션이 스트림으로 저장되어 리플레이, 실시간 테일링, 필터링 및 내보내기가 가능합니다.

데모 새로운 세션이 목록에 나타나고 S2 스트림에서 실시간으로 테일링됩니다. 방문자가 앱을 사용함에 따라 리플레이와 활동 피드가 업데이트됩니다.

빠른 시작 S2 액세스 토큰과 베이슨(basin)이 필요합니다. 이를 .env.local 파일에 설정합니다:

S2_ACCESS_TOKEN=replace-with-an-s2-access-token
S2_BASIN=replaya-your-name
PORT=8787

베이슨은 RePlaya의 스트림 기본값으로 처음 사용 시 생성됩니다. 대시보드의 상태 표시는 베이슨과 유효한 S2 엔드포인트를 보여주어 현재 연결된 대상을 확인할 수 있습니다.

S2 클라우드 대신 s2-lite 또는 다른 호환 배포를 사용하려면 엔드포인트를 명시적으로 설정하세요:

S2_ACCOUNT_ENDPOINT=http://localhost:7070
S2_BASIN_ENDPOINT=http://localhost:7070

그런 다음 종속성을 설치하고 개발 서버를 시작합니다:

pnpm install
pnpm dev

API는 http://localhost:8787에서 실행되고, Vite는 http://localhost:5173에서 대시보드를 제공합니다.

다른 앱을 계측하지 않고 테스트 녹화를 만들려면 http://localhost:8787/recorder-test를 열면 됩니다. 동일한 호스팅된 레코더 스크립트를 통해 녹화됩니다.

프로덕션 스타일의 로컬 실행을 위해서는 빌드하고 Express를 통해 하나의 포트에서 모든 것을 제공합니다:

pnpm build
pnpm start
# http://localhost:8787 열기

드롭인 레코더 캡처를 시작하려면 이 코드를 페이지에 추가하고 RePlaya 호스트를 가리키게 합니다:

<script>
!function(w,d,s,u){w.replaya=w.replaya||function(){(w.replaya.q=w.replaya.q||[]).push(arguments)};var e=d.createElement(s);e.async=1;e.src=u;d.head.appendChild(e)}(window,document,"script","https://replaya.example.com/recorder.js");
replaya("init",{apiHost:"https://replaya.example.com",source:"web-app"});
</script>

로컬 개발에서는 호스트가 http://localhost:8787이며, /recorder-test는 동일한 스크립트를 통해 녹화하는 페이지를 제공합니다.

source는 앱, 사이트, 환경 또는 테넌트별로 캡처를 그룹화하기 위한 선택적 메타데이터입니다. distinctIduserId를 전달하여 세션에 애플리케이션 ID를 태그할 수 있습니다.

기본적으로 레코더는 모든 input, select, textarea 값을 마스킹하므로(rrweb maskAllInputs), 최종 사용자의 키 입력(비밀번호, 이메일, 모든 타이핑)은 서버로 전송되지 않습니다. 허용되는 페이지(예: 내부 관리 UI)에서 원시 양식 제어 상태를 캡처하려면 replaya("init", ...)maskAllInputs: false를 전달하거나 레코더 스크립트 태그에 data-mask-all-inputs="false"를 추가하세요.

마스킹은 입력 값만 포함하며, 페이지가 DOM에 렌더링하는 텍스트는 여전히 기록됩니다. 민감한 영역은 replaya-block 클래스로 감싸서 녹화에서 제외하거나, replaya-ignore를 사용하여 하위 트리의 변경을 건너뛸 수 있습니다.

프로덕션에서는 대시보드와 읽기 API를 비공개로 유지하고 수집기 라우트만 공개적으로 노출하세요.

S2에서의 작동 방식 세션 녹화는 로그입니다. 추가 전용(append-only), 정렬된, 타임스탬프가 있는 이벤트 시퀀스입니다. RePlaya는 각 세션을 하나의 S2 스트림으로 저장하고 동일한 방식으로 다시 읽으므로, 단일 기본 요소로 여러 시스템에 분산되는 기능을 처리할 수 있습니다.

저장소. rrweb 이벤트는 S2 프로듀서 API를 통해 세션 스트림의 끝에 추가됩니다. 이 API는 백프레셔를 통해 배치 처리하고 각 배치가 지속되면 확인 응답을 보냅니다. 스트림 자체가 녹화이며, 별도의 블롭 저장소가 없고 수집과 저장 사이에 서버에서 버퍼링되는 것이 없습니다. 대형 rrweb 이벤트는 여러 S2 레코드에 걸쳐 프레이밍되고 읽을 때 재구성됩니다.

타임라인. 세션 스트림은 timestamping.mode: client-require를 사용하므로 rrweb 캡처 시간이 각 이벤트 레코드의 S2 타임스탬프에 기록되고 다시 읽을 때 타임스탬프로 사용됩니다.

원문 보기
원문 보기 (영어)
RePlaya Self-hosted session replay built on S2 . Each session is stored as one S2 stream, and that stream is the whole backend — there's no separate database, message bus, object store, or search index. Because an S2 stream can be tailed as it's written, RePlaya can replay a session live, while the visitor is still on the page, as well as play back finished ones. Add the recorder snippet to your site and sessions are stored as streams you can replay, live-tail, filter, and export. Demo A new session appears in the list and is live-tailed from its S2 stream — the replay and activity feed update as the visitor uses the app. ( MP4 ) Quickstart You'll need an S2 access token and a basin. Put them in .env.local : S2_ACCESS_TOKEN=replace-with-an-s2-access-token S2_BASIN=replaya-your-name PORT=8787 The basin is created on first use with RePlaya's stream defaults. The dashboard's health pill reports the basin and the effective S2 endpoints so you can confirm what you're pointed at. To use s2-lite or another compatible deployment instead of S2 Cloud, set the endpoints explicitly: S2_ACCOUNT_ENDPOINT=http://localhost:7070 S2_BASIN_ENDPOINT=http://localhost:7070 Then install dependencies and start the dev server: pnpm install pnpm dev The API runs on http://localhost:8787 and Vite serves the dashboard on http://localhost:5173 . To create a test recording without instrumenting another app, open http://localhost:8787/recorder-test ; it records through the same hosted recorder script. For a production-style local run, build and serve everything from Express on one port: pnpm build pnpm start # open http://localhost:8787 Drop-in recorder Add this to any page to start capturing, pointing it at your RePlaya host: < script > ! function ( w , d , s , u ) { w . replaya = w . replaya || function ( ) { ( w . replaya . q = w . replaya . q || [ ] ) . push ( arguments ) } ; var e = d . createElement ( s ) ; e . async = 1 ; e . src = u ; d . head . appendChild ( e ) } ( window , document , "script" , "https://replaya.example.com/recorder.js" ) ; replaya ( "init" , { apiHost : "https://replaya.example.com" , source : "web-app" } ) ; </ script > In local development that host is http://localhost:8787 , and /recorder-test serves a page that records through the same script. source is optional metadata for grouping captures by app, site, environment, or tenant. distinctId and userId can be passed to tag sessions with application identity. By default, the recorder masks all input, select, and textarea values (rrweb maskAllInputs ), so end-user keystrokes — passwords, emails, anything typed — are never sent to the server. To capture raw form control state on a page where that's acceptable (e.g. an internal admin UI), pass maskAllInputs: false to replaya("init", ...) , or add data-mask-all-inputs="false" to the recorder script tag. Masking covers input values only; text the page renders into the DOM is still recorded. Wrap sensitive regions in the replaya-block class to omit them from the recording, or replaya-ignore to skip a subtree's changes. In production, keep the dashboard and read APIs private and expose only the collector routes publicly; see Configuration & deployment . How it works on S2 A session recording is a log: an append-only, ordered, timestamped sequence of events. RePlaya stores each session as one S2 stream and reads it back the same way, so a single primitive covers what's often split across several systems. Storage. rrweb events are appended to the tail of the session's stream over the S2 Producer API, which batches with backpressure and acks each batch once it's durable. The stream is the recording — there's no separate blob store, and nothing buffers on the server between ingest and storage. Large rrweb events are framed across multiple S2 records and reconstructed on read. Timeline. Session streams use timestamping.mode: client-require , so the rrweb capture time is written into each event record's S2 timestamp and read back as the scrub timeline. (Create, stop, and heartbeat records use server wall-clock time.) Listing. S2 lists streams in lexicographic order, so each stream is named sessions/<inverted timestamp> ; streams.list({ prefix: "sessions/" }) then returns newest-first with startAfter paging — no database keeping an order in sync. A best-effort sidecar index stream is tailed to update the list as new sessions start. Live tail. GET /api/sessions/:id/live opens an S2 read session from the snapshot tail and bridges new records to the browser over SSE, where they're appended to the mounted player. The same stream serves both the historical scrub and the live edge. Concurrency. Stream creation and stop write active / stopped fencing tokens; event and heartbeat appends are fenced on active , so a finished session can't be resurrected by a late writer. Streams are created on first append ( createStreamOnAppend ), inheriting the basin's default config, so there's no stream provisioning to manage. That leaves one external dependency: point RePlaya at S2 Cloud , or at a self-hosted s2-lite to keep everything in your own infrastructure. Recordings live in your own basin — URI-addressable, with configurable retention and on-demand deletion. The browser never receives the S2 token; all S2 reads and writes go through the RePlaya server. For comparison with a typical session-replay backend: Typical replay backend RePlaya Services to run Message bus, analytics store, relational DB, object store, search index One Node server + S2 Live sessions Usually playback after an ingest/flush delay Live tail of active sessions, off the same stream Stored recording Blobs in object storage; metadata across databases One ordered S2 stream per session Self-host footprint A multi-service cluster, often on Kubernetes A single process + S2 (or self-hosted s2-lite) Dashboard search is a client-side filter over the sessions already listed — S2 provides ordering and newest-first listing, not full-text search. See ARCHITECTURE.md for the full design. Configuration & deployment Server-only S2 configuration lives in .env.local (see Quickstart ). The read side has no built-in authentication by design — the deployment boundary is the access control. In production, RePlaya treats the collector as public, write-only surface area and the dashboard/read APIs as private surface area. Don't expose the read APIs to the internet. Bind the app to a private interface and put the dashboard, GET /api/sessions* , live tailing, and /api/health behind your SSO/access layer (VPN, Tailscale, Cloudflare Access, oauth2-proxy). Expose only the collector publicly — GET /recorder.js , GET /vendor/rrweb.min.js , and the write endpoints ( POST /api/sessions , /events , /heartbeat , /stop ) — and lock those down with REPLAYA_ALLOWED_CAPTURE_ORIGINS + REPLAYA_PROJECT_KEY . Set NODE_ENV=production so ingest auth is enforced, originless ingest is rejected, and the recorder test fixture is disabled. Set REPLAYA_APPEND_TOKEN_SECRET to a stable random value (e.g. openssl rand -hex 32 ). With ingest auth enabled (the production default), the server refuses to start without it. Set REPLAYA_TRUST_PROXY=true if you run behind a reverse proxy so per-client rate limits use the real client IP. NODE_ENV=production REPLAYA_PROJECT_KEY=pk_live_replace_with_public_write_key REPLAYA_APPEND_TOKEN_SECRET=replace-with-a-long-random-secret REPLAYA_ALLOWED_CAPTURE_ORIGINS=https://app.example.com,https://www.example.com REPLAYA_TRUST_PROXY=true Then pass the public project key in the recorder init: replaya ( "init" , { apiHost : "https://collect.example.com" , projectKey : "pk_live_replace_with_public_write_key" , source : "web-app" } ) ; Session create returns a short-lived append token that the recorder sends with event, heartbeat, and stop writes. Useful limits and knobs: REPLAYA_SESSION_CREATE_RATE_LIMIT default 60 per minute per client/project. REPLAYA_SESSION_APPEND_RATE_LIMIT default 600 per minute per client/session. REPLAYA_MAX_