feat: add session replay functionality with privacy controls

This commit is contained in:
Usman Baig
2026-01-19 12:43:02 +01:00
parent c8695ff964
commit a740b0d703
8 changed files with 1434 additions and 12 deletions

112
lib/api/replays.ts Normal file
View File

@@ -0,0 +1,112 @@
import apiRequest from './client'
export interface ReplayListItem {
id: string
session_id: string
started_at: string
ended_at: string | null
duration_ms: number
events_count: number
device_type: string | null
browser: string | null
os: string | null
country: string | null
entry_page: string
is_skeleton_mode: boolean
}
export interface ReplayFilters {
device_type?: string
country?: string
min_duration?: number
limit?: number
offset?: number
}
export interface ReplayListResponse {
replays: ReplayListItem[]
total: number
limit: number
offset: number
}
export interface SessionReplay extends ReplayListItem {
site_id: string
consent_given: boolean
created_at: string
expires_at: string
}
export async function listReplays(
siteId: string,
filters?: ReplayFilters
): Promise<ReplayListResponse> {
const params = new URLSearchParams()
if (filters?.device_type) params.set('device_type', filters.device_type)
if (filters?.country) params.set('country', filters.country)
if (filters?.min_duration) params.set('min_duration', filters.min_duration.toString())
if (filters?.limit) params.set('limit', filters.limit.toString())
if (filters?.offset) params.set('offset', filters.offset.toString())
const queryString = params.toString()
const url = `/sites/${siteId}/replays${queryString ? `?${queryString}` : ''}`
return apiRequest<ReplayListResponse>(url)
}
export async function getReplay(siteId: string, replayId: string): Promise<SessionReplay> {
return apiRequest<SessionReplay>(`/sites/${siteId}/replays/${replayId}`)
}
export async function getReplayData(siteId: string, replayId: string): Promise<unknown[]> {
const response = await apiRequest<unknown[]>(`/sites/${siteId}/replays/${replayId}/data`)
return response
}
export async function deleteReplay(siteId: string, replayId: string): Promise<void> {
await apiRequest(`/sites/${siteId}/replays/${replayId}`, {
method: 'DELETE',
})
}
// Utility function to format replay duration
export function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`
const seconds = Math.floor(ms / 1000)
if (seconds < 60) return `${seconds}s`
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`
const hours = Math.floor(minutes / 60)
const remainingMinutes = minutes % 60
return `${hours}h ${remainingMinutes}m`
}
// Utility function to get device icon
export function getDeviceIcon(deviceType: string | null): string {
switch (deviceType?.toLowerCase()) {
case 'mobile':
return '📱'
case 'tablet':
return '📱'
case 'desktop':
default:
return '💻'
}
}
// Utility function to get browser icon
export function getBrowserIcon(browser: string | null): string {
switch (browser?.toLowerCase()) {
case 'chrome':
return '🌐'
case 'firefox':
return '🦊'
case 'safari':
return '🧭'
case 'edge':
return '🌀'
default:
return '🌐'
}
}

View File

@@ -1,6 +1,7 @@
import apiRequest from './client'
export type GeoDataLevel = 'full' | 'country' | 'none'
export type ReplayMode = 'disabled' | 'consent_required' | 'anonymous_skeleton'
export interface Site {
id: string
@@ -17,6 +18,12 @@ export interface Site {
collect_device_info?: boolean
collect_geo_data?: GeoDataLevel
collect_screen_resolution?: boolean
// Session replay settings
replay_mode?: ReplayMode
replay_sampling_rate?: number
replay_retention_days?: number
replay_mask_all_text?: boolean
replay_mask_all_inputs?: boolean
created_at: string
updated_at: string
}
@@ -39,6 +46,12 @@ export interface UpdateSiteRequest {
collect_device_info?: boolean
collect_geo_data?: GeoDataLevel
collect_screen_resolution?: boolean
// Session replay settings
replay_mode?: ReplayMode
replay_sampling_rate?: number
replay_retention_days?: number
replay_mask_all_text?: boolean
replay_mask_all_inputs?: boolean
}
export async function listSites(): Promise<Site[]> {