npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@replayio-app-building/help-widget

v0.18.0

Published

Embeddable help widget with element picker and AI chat for React apps

Readme

@replayio-app-building/help-widget

Embeddable help widget with AI chat, element picker, and React component chain detection.

Installation

npm install @replayio-app-building/help-widget @replayio-app-building/session-recorder

Peer Dependencies

Required:

  • react ^18.0.0 || ^19.0.0
  • react-dom ^18.0.0 || ^19.0.0
  • @replayio-app-building/session-recorder ^0.7.6

Server (required when using ./server exports):

  • @neondatabase/serverless ^0.10.0
  • @anthropic-ai/sdk ^0.39.0

Client Setup

1. Initialize Session Recorder

Session recording must be initialized before your app renders. Call startSession() in your app entry point (e.g. main.tsx) and store the handle globally so the widget can access it later:

// main.tsx
import { startSession, type SessionHandle } from '@replayio-app-building/session-recorder'

declare global {
  interface Window {
    __sessionHandle?: SessionHandle
  }
}

// Initialize BEFORE createRoot — starts capturing DOM, network, and interaction events
window.__sessionHandle = startSession()

startSession() returns a SessionHandle with:

  • sessionId — unique identifier for this session
  • getSessionData() — returns all captured simulation data (DOM mutations, network requests, user interactions, errors)

2. Render the Widget

import { HelpWidget } from '@replayio-app-building/help-widget'
import '@replayio-app-building/help-widget/styles.css'

function App() {
  return (
    <HelpWidget
      appName="My App"
      apiBase="/api"
      position="bottom-right"
      theme={{ primaryColor: '#6366f1', fontFamily: 'Inter, sans-serif' }}
    />
  )
}

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | appName | string | required | Display name shown in the chat header | | apiBase | string | '/api' | Base URL for API endpoints | | position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Widget position | | theme | { primaryColor?: string; fontFamily?: string } | — | Optional theme overrides | | getAppState | () => unknown | — | Returns current app state, sent with each message for context | | getPageContext | () => PageContext \| null | — | Override how page context (route + visible DOM) is captured. By default the widget captures this automatically; return null to opt out. See Page Context | | pageContextOptions | CapturePageContextOptions | — | Tuning for the default page-context capture (maxDomChars, maxDepth, root). Ignored when getPageContext is set | | onRefreshState | () => void | — | Called when the assistant performs a tool action that modifies app data | | onNavigate | (path: string) => void | — | Called when the assistant uses the navigate_user tool | | getSessionData | () => Promise<{ sessionId: string }> | — | Returns session recording data for bug report attachment | | renderHooks | RenderHooks | — | Custom rendering hooks to override widget UI sections | | onEvent | (event: HelpWidgetEvent) => void | — | Called on every user action involving the widget (open, close, send message, element picking, etc). See Event Callbacks | | versionCheckUrl | string | — | Webhook returning the currently deployed version. When set, the widget polls it and shows a reload prompt once the version changes. See Deploy Version Checking | | versionPollIntervalMs | number | 120000 | How often to poll versionCheckUrl (default 2 minutes) | | onUpdateAvailable | () => void | — | Called once, the first time a newer deploy is detected |

Page Context

The widget automatically captures a standardized page context with every message so the assistant consistently knows what page the user is on and what DOM they are looking at — without any per-app wiring. The captured PageContext is:

interface PageContext {
  url: string    // window.location.href
  path: string   // pathname + search + hash (the in-app route)
  title: string  // document.title
  dom: string    // trimmed, indented outline of the visible DOM
}

The DOM outline excludes the widget's own elements, scripts/styles, and hidden nodes, and is size-bounded so the payload stays small on large pages. The server prepends this context to the latest user turn before calling the model, formatted identically for every app.

This is on by default. To customize it:

<HelpWidget
  appName="My App"
  // Tune the default capture
  pageContextOptions={{ maxDomChars: 8000, maxDepth: 20 }}
  // ...or fully override / opt out (return null to send no page context)
  getPageContext={() => capturePageContext({ root: document.querySelector('#app') })}
/>

capturePageContext is also exported standalone for custom use.

Connecting Session Recording to the Widget

The getSessionData prop connects the session recorder to the widget. When a user submits a bug report, the widget calls this function to upload the captured session and get a session ID:

import { useCallback } from 'react'
import { uploadSession } from '@replayio-app-building/session-recorder'
import { HelpWidget } from '@replayio-app-building/help-widget'
import '@replayio-app-building/help-widget/styles.css'

function App() {
  const getSessionData = useCallback(async () => {
    const handle = window.__sessionHandle
    if (!handle) throw new Error('No session handle')
    const result = await uploadSession(handle.sessionId, handle.getSessionData())
    return { sessionId: result.session_id }
  }, [])

  return (
    <HelpWidget
      appName="My App"
      apiBase="/api"
      getSessionData={getSessionData}
      onRefreshState={() => { /* re-fetch your app data */ }}
    />
  )
}

uploadSession() gzip-compresses the session data and uploads it to the /api/upload-session endpoint. The returned session_id is stored on the conversation so the server can later create a Replay recording from it.

Custom Rendering Hooks

The renderHooks prop lets embedders override individual UI sections without forking the widget. Each hook receives the same props the default component uses, so you can selectively customize rendering while keeping the widget's state management and polling logic.

import { HelpWidget } from '@replayio-app-building/help-widget'
import type { RenderHooks } from '@replayio-app-building/help-widget'

const renderHooks: RenderHooks = {
  renderButton: ({ isOpen, onClick, position }) => (
    <button onClick={onClick}>
      {isOpen ? 'Close' : 'Help'}
    </button>
  ),
  renderHeader: ({ onClose, onReset }) => (
    <div className="my-header">
      <span>Support</span>
      <button onClick={onClose}>×</button>
    </div>
  ),
}

function App() {
  return <HelpWidget appName="My App" renderHooks={renderHooks} />
}

Available hooks:

| Hook | Props | Description | |------|-------|-------------| | renderButton | RenderButtonProps | Replace the floating help button | | renderHeader | RenderHeaderProps | Replace the chat header bar | | renderMessage | RenderMessageProps | Replace individual message bubbles | | renderInput | RenderInputProps | Replace the chat text input area | | renderTypingIndicator | () => ReactNode | Replace the typing animation | | renderPanel | RenderPanelProps | Replace the entire chat panel container (receives children as a prop) |

When a hook is not provided, the default built-in component renders. All default sub-components are exported so you can compose them in custom layouts.

Event Callbacks

The onEvent prop is called every time the user takes an action involving the widget. Use it for analytics, telemetry, or to drive your own UI in response to widget interactions. It receives a single HelpWidgetEvent, a discriminated union keyed on type:

import { HelpWidget } from '@replayio-app-building/help-widget'
import type { HelpWidgetEvent } from '@replayio-app-building/help-widget'

function App() {
  const handleEvent = (event: HelpWidgetEvent) => {
    switch (event.type) {
      case 'message_sent':
        analytics.track('help_message_sent', { message: event.message })
        break
      case 'open':
        analytics.track('help_opened')
        break
      default:
        analytics.track(`help_${event.type}`)
    }
  }

  return <HelpWidget appName="My App" onEvent={handleEvent} />
}

onEvent is purely observational — it never affects the widget's own behavior, and is always called synchronously as part of handling the action. Every user action emits exactly one event.

Events:

| event.type | Payload | Fired when | |--------------|---------|------------| | open | — | The user opens the chat panel | | close | — | The user closes the chat panel | | message_sent | message: string, elementContext?: ElementContext | The user sends a message (includes any selected element context) | | element_pick_started | — | The user activates the element picker (pointer button) | | element_selected | elementContext: ElementContext | The user clicks an element on the page while picking | | element_pick_cancelled | — | The user cancels picking (cancel button or Escape key) | | element_context_cleared | — | The user clears the selected element context | | conversation_reset | — | The user resets the conversation |

HelpWidgetEvent and its type discriminant HelpWidgetEventType are both exported for use in your own typings.

Deploy Version Checking

When a new version of your app is deployed, users with the page already open keep running the old bundle until they reload. The widget can detect this for you: point versionCheckUrl at an endpoint that returns the currently deployed version (e.g. a git SHA), and the widget polls it in the background. As soon as the reported version differs from the one seen on first load, it shows a "new version available" indicator — a red badge on the help button and a Reload prompt at the top of the chat panel. Clicking Reload calls window.location.reload().

import { HelpWidget } from '@replayio-app-building/help-widget'

function App() {
  return (
    <HelpWidget
      appName="My App"
      versionCheckUrl="/api/deployed-version"   // polled every 2 minutes by default
      versionPollIntervalMs={120000}            // optional override
      onUpdateAvailable={() => analytics.track('update_available')} // optional
    />
  )
}

How it works:

  • Polling starts on mount and runs every versionPollIntervalMs (default 120000 — two minutes). Requests are sent with cache: 'no-store'.
  • The first successful response establishes the baseline version. Each later response is compared against it.
  • The moment a different version is seen, the indicator appears, onUpdateAvailable fires once, and polling stops (a reload is required to pick up the new deploy, so there is nothing left to watch).
  • Transient errors and non-2xx responses are ignored; polling simply retries on the next tick.

Response format: the endpoint may return the version as plain text (the whole body is the version) or as a JSON object, in which case the first present of sha, commit, or version is used:

{ "sha": "a1b2c3d4" }

Server endpoint (Netlify example): Netlify injects the build's commit ref into the function runtime as COMMIT_REF, so a one-line function is enough:

// netlify/functions/deployed-version.ts
export default async () =>
  new Response(JSON.stringify({ sha: process.env.COMMIT_REF || 'unknown' }), {
    headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
  })

The useVersionCheck hook backing this behavior is also exported if you want to drive your own UI from it:

import { useVersionCheck } from '@replayio-app-building/help-widget'

const updateAvailable = useVersionCheck({ url: '/api/deployed-version' })

When using renderHooks.renderButton, the updateAvailable flag is passed through in RenderButtonProps so a custom button can render its own indicator.

Auto-Capturing User Sessions

The widget can continuously capture the user's whole session (DOM mutations, network, interactions, and rrweb events) and stream it to your backend, independent of the bug-report flow. This is optional — call startAutoCapture() once at startup to enable it.

A session id is created on startup. Every few seconds (3s by default) the data captured since the previous flush is sent as a numbered, gzip-compressed chunk to {apiBase}/append-session-chunk, where the server stores it in the help_widget_session_chunks table. Later you can reassemble the chunks, read the rrweb data, and create a Replay recording for the session using the server helpers.

1. Start capture on the client

// main.tsx — call once, before/around rendering your app
import { startAutoCapture } from '@replayio-app-building/help-widget'

const capture = startAutoCapture({
  apiBase: '/api',        // default '/api'
  flushIntervalMs: 3000,  // default 3000 (every 3 seconds)
})

// `capture.sessionId` — the id the chunks are stored under (persist/log it if
//   you want to look up the recording later).
// `capture.flush()` — force an immediate flush.
// `capture.stop()`  — flush remaining data and stop the interval.

By default startAutoCapture() starts its own session recording. If you already call startSession() at startup (see Client Setup), pass that handle so a single recording is shared:

import { startSession } from '@replayio-app-building/session-recorder'
import { startAutoCapture } from '@replayio-app-building/help-widget'

const handle = startSession()
window.__sessionHandle = handle
const capture = startAutoCapture({ handle })   // reuses the same session id + data

AutoCaptureOptions:

| Option | Type | Default | Description | |--------|------|---------|-------------| | apiBase | string | '/api' | Base URL for the append-session-chunk endpoint | | flushIntervalMs | number | 3000 | How often to flush captured data to the backend | | sessionId | string | generated | Override the session id chunks are stored under | | handle | SessionHandle | new session | Reuse an existing session-recorder handle instead of starting a new one | | onError | (error: unknown) => void | — | Called when a flush fails (capture continues on the next interval) |

Flushes are incremental (only new packets are sent), serialized, and resilient: a failed flush re-sends the same packets on the next tick, and chunk indexes stay monotonic and contiguous, so no data is dropped or duplicated. A final best-effort flush fires on pagehide/beforeunload.

2. Provide the append endpoint on the server

// netlify/functions/append-session-chunk.ts
import { neon } from '@neondatabase/serverless'
import { createAppendSessionChunkHandler } from '@replayio-app-building/help-widget/server'

const handler = createAppendSessionChunkHandler({ sql: neon(process.env.DATABASE_URL!) })

export default async (req: Request) => handler(req)

The handler accepts the gzip-compressed body (or plain JSON) and is idempotent per (sessionId, chunkIndex).

3. Read the session / create a recording (server)

import { neon } from '@neondatabase/serverless'
import {
  getUserSessionData,
  getUserSessionRRWebEvents,
  extractRRWebEvents,
  createUserSessionRecording,
} from '@replayio-app-building/help-widget/server'

const sql = neon(process.env.DATABASE_URL!)

// Reassemble the full session (all packets, in capture order).
const data = await getUserSessionData(sql, sessionId)

// Just the rrweb events (equivalent to extractRRWebEvents(data)).
const events = await getUserSessionRRWebEvents(sql, sessionId)

// Produce (or fetch) the Replay recording for the session.
const getRecording = createUserSessionRecording({ sql, baseUrl: process.env.URL! })
const recording = await getRecording(sessionId)
// → { recordingId, recordingUrl } | null

createUserSessionRecording reassembles the session, uploads it via the app's own /api/upload-session endpoint (compressed on the wire), and creates a Replay recording. If a recording already exists for the session id it is returned without re-uploading. It requires the session-recorder server endpoints (upload-session, session-recording) to be wired up — see Session Recorder Server Endpoints.

API Endpoints

The client expects these endpoints relative to apiBase:

  • POST {apiBase}/new-help-widget-message-background — Send a new message. Body: { conversationId, message, elementContext?, appState? }. Should return 202 (background processing).
  • GET {apiBase}/poll-help-widget-conversation?conversationId=... — Poll for response. Returns { status, messages, partialResponse, pendingActions }.
  • POST {apiBase}/provide-help-widget-session-data — Provide session recording data. Body: { conversationId, bugReportId, sessionId }. Called by the client when the create_session_url pending action is received; bugReportId is read from that action and identifies the bug-report row the session belongs to.
  • POST {apiBase}/append-session-chunk — Append one auto-captured session chunk. Gzip-compressed (Content-Encoding: gzip, application/octet-stream) or plain JSON body: { sessionId, chunkIndex, data }. Called by startAutoCapture on every flush. Idempotent per (sessionId, chunkIndex).

Server Setup

The ./server entry point provides helpers for setting up the database and request handlers.

import {
  initHelpWidgetSchema,
  createNewMessageHandler,
  createPollHandler,
  createProvideSessionDataHandler,
  createRecordingCompleteHandler,
} from '@replayio-app-building/help-widget/server'

Database Schema

Call initHelpWidgetSchema once during your schema initialization to create the required tables:

import { neon } from '@neondatabase/serverless'
import { initHelpWidgetSchema } from '@replayio-app-building/help-widget/server'

const sql = neon(process.env.DATABASE_URL!)
await initHelpWidgetSchema(sql)

This creates these tables:

  • help_widget_conversations — Tracks conversation state (id, status, partial_response, pending_actions, recorder_session_id, timestamps)
  • help_widget_messages — Stores all messages (conversation_id, role, content, element_context, app_state, created_at)
  • help_widget_bug_reports — One row per submitted bug report (id, conversation_id, body, session_id, recording_id, status, webhook_sent, timestamps). The status column advances awaiting_sessionrecordingrecorded | failed | discrepancy. Per-bug rows keep concurrent bug reports (and follow-up messages) from clobbering each other's in-flight recording.
  • help_widget_session_chunks — One row per auto-captured session flush (id, session_id, chunk_index, data, created_at), unique on (session_id, chunk_index). Ordering by chunk_index and concatenating the data arrays reassembles the full session (see getUserSessionData).

Request Handlers

createNewMessageHandler(options)

Creates a handler that processes incoming messages by calling Claude with tool use, streaming partial responses to the database.

const handler = createNewMessageHandler({
  sql: neon(process.env.DATABASE_URL!),
  anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
  systemPrompt: 'You are a helpful assistant.',
  model: 'claude-opus-4-6',
  tools: [
    {
      name: 'list_items',
      description: 'List all items',
      inputSchema: { type: 'object', properties: {} },
      execute: async (input) => { /* ... */ },
    },
  ],
  routes: [{ path: '/', description: 'Main page' }],
  bugReportWebhookUrl: process.env.BUG_REPORT_WEBHOOK_URL,
  baseUrl: process.env.URL,
})

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | sql | SqlFunction | required | Neon SQL tagged template function | | anthropicApiKey | string | required | Anthropic API key | | systemPrompt | string | Generic helpful assistant prompt | System prompt for Claude | | model | string | 'claude-opus-4-6' | Claude model to use | | tools | ToolDefinition[] | [] | Custom tools Claude can call (each has name, description, inputSchema, execute) | | routes | RouteDefinition[] | [] | App routes for the built-in navigate_user tool | | bugReportWebhookUrl | string | — | Webhook URL for submitted reports. Enables the built-in submit_bug_report tool, which handles feature and change requests as well as bugs | | baseUrl | string | — | App base URL. Required for session recording attachment to bug reports | | sessionWaitMs | number | 60000 | How long to wait for the client's session upload before finalizing a bug report without a recording |

Built-in tools (added automatically):

  • navigate_user — Always added. Queues a navigate pending action for the client.
  • submit_bug_report — Added when bugReportWebhookUrl is set. The single channel for both bug reports and feature/change requests: it takes an optional request_type ('bug' | 'feature' | 'change') and a body, labels the forwarded report accordingly, and attaches a session recording if baseUrl is also set. The default system prompt and the tool description steer the model to use it for feature requests too, not just bugs.

Bug report lifecycle: When submit_bug_report is called, the handler inserts a row into help_widget_bug_reports (status awaiting_session) and queues a create_session_url pending action carrying that row's bugReportId. The client uploads its session and calls provide-help-widget-session-data with the bugReportId, which records the session_id (status recording). The handler then polls that bug-report row for the session id, creates the Replay recording, writes the recording_id back (status recorded), and sends a single webhook call with both the bug body and recording URL. If the recording fails or times out, the webhook is sent with just the bug body as a fallback (status failed); on a recording discrepancy the webhook is skipped (status discrepancy). webhook_sent is set once the webhook fires. The widget owns the entire recording lifecycle — createRecordingFromSession is the sole trigger for session recording creation.

If a single user message causes the model to call submit_bug_report more than once, every filed report is finalized — each gets its own row, recording poll, and webhook delivery — not just the last one.

Keying the wait off the per-bug row (rather than a single column on the conversation) is what lets a second message — or a second bug report — proceed without clobbering an earlier report's in-flight recording.

createPollHandler(options)

Creates a handler that returns the current conversation state.

const handler = createPollHandler({
  sql: neon(process.env.DATABASE_URL!),
})

Returns JSON: { status: string, messages: Array, partialResponse: string | null, pendingActions: ClientAction[] }

Pending actions are cleared from the database after being returned, so each action is delivered exactly once.

createProvideSessionDataHandler(options)

Creates a handler for the client to provide session recording data. The client calls this when it receives a create_session_url pending action, passing the bugReportId from that action. Stores the session_id on the matching help_widget_bug_reports row (and advances its status to recording) so the message handler can trigger recording creation. Body: { conversationId, bugReportId, sessionId }. If bugReportId is omitted (older clients), the session is attached to the most recent bug report on the conversation that is still awaiting one.

const handler = createProvideSessionDataHandler({
  sql: neon(process.env.DATABASE_URL!),
})

Important: This handler only stores the session ID. It does not trigger session recording creation — that is handled internally by createNewMessageHandler after the conversation loop.

createRecordingCompleteHandler(options)

Handles the recording-complete webhook callback. Delegates to @replayio-app-building/session-recorder/server for the core logic to mark recordings as complete in the database.

const handler = createRecordingCompleteHandler({
  sql: neon(process.env.DATABASE_URL!),
})

Options:

| Option | Type | Description | |--------|------|-------------| | sql | SqlFunction | Neon SQL tagged template function |

Requires @replayio-app-building/session-recorder as a peer dependency.

Netlify Functions Example

netlify/functions/new-help-widget-message-background.ts (the -background suffix gives it a 15-minute timeout):

import { neon } from '@neondatabase/serverless'
import { createNewMessageHandler } from '@replayio-app-building/help-widget/server'

const handler = createNewMessageHandler({
  sql: neon(process.env.DATABASE_URL!),
  anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
  systemPrompt: 'You are a helpful assistant for My App.',
  bugReportWebhookUrl: process.env.BUG_REPORT_WEBHOOK_URL,
  baseUrl: process.env.URL,
})

export default async (req: Request) => {
  await handler(req)
}

netlify/functions/poll-help-widget-conversation.ts:

import { neon } from '@neondatabase/serverless'
import { createPollHandler } from '@replayio-app-building/help-widget/server'

const handler = createPollHandler({
  sql: neon(process.env.DATABASE_URL!),
})

export default async (req: Request) => {
  return handler(req)
}

netlify/functions/provide-help-widget-session-data.ts:

import { neon } from '@neondatabase/serverless'
import { createProvideSessionDataHandler } from '@replayio-app-building/help-widget/server'

const handler = createProvideSessionDataHandler({
  sql: neon(process.env.DATABASE_URL!),
})

export default async (req: Request) => {
  return handler(req)
}

Session Recorder Server Endpoints

The session recording flow requires additional server endpoints from @replayio-app-building/session-recorder/server. These handle session upload, recording creation, and data retrieval:

netlify/functions/upload-session.ts — Receives gzipped session data from the client:

import { createUploadSessionHandler } from '@replayio-app-building/session-recorder/server'
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)
const handler = createUploadSessionHandler({ sql })

export default async (req: Request) => {
  return handler(req)
}

netlify/functions/session-recording.ts — Creates a Replay recording from a stored session:

import { createSessionRecordingHandler } from '@replayio-app-building/session-recorder/server'
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)
const handler = createSessionRecordingHandler({
  sql,
  baseUrl: process.env.URL || '',
  sessionRecorderUrl: 'https://session-recorder-si6nol.netlify.app',
  pollTimeout: 0,
})

export default async (req: Request) => {
  return handler(req)
}

netlify/functions/recording-complete.ts — Webhook called when a recording finishes processing. Use createRecordingCompleteHandler from the help-widget server exports (it wraps the session-recorder handler to mark recordings complete in the database). Bug report webhook submission is handled exclusively by createNewMessageHandler:

import { createRecordingCompleteHandler } from '@replayio-app-building/help-widget/server'
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)
const handler = createRecordingCompleteHandler({ sql })

export default async (req: Request) => {
  return handler(req)
}

netlify/functions/session-data.ts — Retrieves stored session data by ID:

import { createSessionDataHandler } from '@replayio-app-building/session-recorder/server'
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)
const handler = createSessionDataHandler({ sql })

export default async (req: Request) => {
  return handler(req)
}

netlify/functions/recordings.ts — Lists recordings or retrieves a specific recording:

import { createRecordingsHandler } from '@replayio-app-building/session-recorder/server'
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)
const handler = createRecordingsHandler({ sql })

export default async (req: Request) => {
  return handler(req)
}

URL Routing for Session Recorder Endpoints

Add redirects in netlify.toml for the session recorder endpoints:

[[redirects]]
  from = "/api/session-data/*"
  to = "/.netlify/functions/session-data"
  status = 200

[[redirects]]
  from = "/api/recordings/*"
  to = "/.netlify/functions/recordings"
  status = 200

The other session recorder endpoints (upload-session, session-recording, recording-complete) are covered by the general /api/* redirect.

Required Environment Variables

| Variable | Description | |----------|-------------| | DATABASE_URL | PostgreSQL connection string (Neon serverless) | | ANTHROPIC_API_KEY | Anthropic API key for Claude | | BUG_REPORT_WEBHOOK_URL | Optional webhook URL for bug reports | | URL | App base URL (used for session recording attachment) |

URL Routing

If using Netlify, add a redirect in netlify.toml so /api/* routes to functions:

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200

Element Picker

The widget includes a built-in element picker. Users click the pointer button in the chat header, then click any element on the page. The widget captures:

  • The HTML element tag
  • The visible text content
  • The React component chain (traversed via React fiber tree)

This context is attached to the next message and included in the Claude prompt for more contextual responses.

Exports

Client (@replayio-app-building/help-widget)

Components:

  • HelpWidget — Main React component
  • HelpButton — Floating help/close button
  • ChatHeader — Chat panel header with pick element, reset, and close buttons
  • ChatPanel — Chat panel with messages, input, and typing indicator
  • ChatInput — Text input with send button
  • MessageBubble — Individual message display
  • TypingIndicator — Animated typing dots
  • ElementContextDisplay — Selected element context display

Hooks & Utilities:

  • useElementPicker — Hook for element picking functionality
  • useVersionCheck — Hook that polls a deployed-version webhook and flags when a newer deploy is live
  • getReactComponentChain — Utility to get React component chain from a DOM element
  • capturePageContext — Capture the standardized page context (route + visible DOM outline) the widget sends with each message
  • startAutoCapture — Start auto-capturing the user session and flushing chunks to the backend

Types:

  • HelpWidgetProps, HelpWidgetEvent, HelpWidgetEventType, Message, ElementContext
  • PageContext, CapturePageContextOptions
  • AutoCaptureOptions, AutoCaptureHandle
  • RenderHooks, RenderButtonProps, RenderHeaderProps, RenderMessageProps, RenderInputProps, RenderPanelProps

Server (@replayio-app-building/help-widget/server)

  • initHelpWidgetSchema — Creates required database tables
  • createNewMessageHandler — Factory for the message processing handler
  • createPollHandler — Factory for the polling handler
  • createProvideSessionDataHandler — Factory for the session data endpoint
  • createRecordingCompleteHandler — Factory for the recording-complete webhook handler
  • createAppendSessionChunkHandler — Factory for the auto-capture chunk-append endpoint
  • getUserSessionData — Reassemble a captured user session from its chunks
  • getUserSessionRRWebEvents — Reassemble a session and return just its rrweb events
  • extractRRWebEvents — Extract rrweb events from reassembled session data
  • createUserSessionRecording — Produce (or fetch) the Replay recording for a captured session

Types:

  • SqlFunction, ToolDefinition, ClientAction, RouteDefinition, PageContext