Session Recording
Your on-premise instance can record Sessions and render them to MP4, with the video files never leaving your infrastructure. For recording to work you need:
- The session recording feature on your plan. It's embedded in your license key, so if you just added it, re-download the license key (or use the URL form of
LICENSE_KEY) and restart the server. - MongoDB (
MONGO_URImust not be disabled): Session events used for the replay are stored there. - For Sessions that include browser co-browsing, a separate Chromium container the server can reach over WebSocket, described below.
Sessions made only of screen sharing frames are rendered with ffmpeg directly and don't need the Chromium container (if you run the binaries instead of Docker, make sure ffmpeg is available on the PATH).
The Chromium Container
Browser co-browsing Sessions are replayed in a real browser. Use our upscope/chromium image:
docker run -d -e TOKEN=mytoken -p 3000:3000 upscope/chromium
Then point your Co-Browsing API instance at it with CHROMIUM_ENDPOINT=ws://yourdockerhost:3000?token=mytoken.
The container is configured through environment variables:
| Environment Variable | Description | Default |
|---|---|---|
TOKEN | The shared secret used to authenticate connections, passed in the endpoint's ?token= query. When unset, authentication is disabled entirely. | (nil) |
PORT | The port the container listens on. | 3000 |
POOL_MIN | The number of browsers kept warm and ready. | 1 |
POOL_MAX | The maximum number of concurrent browsers. Past this, requests queue. | 10 |
QUEUE_MAX_WAIT | How long a request waits for a free browser before failing, in milliseconds. | 8000 |
TIMEOUT | The maximum lifetime of a single browser session, in milliseconds. | 3600000 |
GOTO_TIMEOUT | The navigation timeout for the container's /screenshot and /pdf HTTP endpoints, in milliseconds. Not used for Session replays. | 10000 |
SHUTDOWN_GRACE | How long in-flight work gets to finish after SIGTERM before the process exits, in milliseconds. | 10000 |
LOG_CONNECTIONS | Log a line for each new connection from your Co-Browsing API instance. Set to false to disable. | true |
STATS_INTERVAL_MS | The interval of the periodic pool and load stats log line, in milliseconds. 0 disables it. | 60000 |
DEBUG | Debug log namespaces, e.g. upscope:*. | (nil) |
Fonts in Replays
/usr/share/fonts/custom and the container picks them up on start.Requesting a Recording
Nothing is rendered until you ask for it. Once a Session has ended, POST its session_id to {BASE_ENDPOINT}/api/recordings/{visitor_short_id}, authenticated with a short-lived HS256 token signed with your SECRET_KEY. The first call starts the render, later calls report progress, and the video URL comes back once it's ready. See Requesting a Session Recording for the full request and its responses.
A Session can't be rendered if no events were stored for it, if it needs a browser and CHROMIUM_ENDPOINT is unset, or if MongoDB is disabled.
You can skip recording an individual Session with the session_properties.record parameter on the watch link and viewer token endpoints.
When a Render Fails
A render that fails reports one of a closed set of error codes, listed with the endpoint under Failed Renders. Most of them name the problem outright. recording_failed is the exception: it's the bucket for a failure we couldn't attribute, and the underlying error is only in your instance's logs.
Logging is off by default. Set DEBUG to upscope:recordings:,upscope:app:errors and restart the server: upscope:app:errors carries the stack trace of the unattributed failure, and upscope:recordings: traces the render's progress, including the stage it was in when it failed. The FFmpeg output goes straight to the server's own stdout, so a failed encode shows up in the container logs whether or not DEBUG is set.
Renders that fail on some Sessions while others on the same instance succeed usually come down to one of:
- Disk. Frames are written to
/tmpbefore FFmpeg consolidates them, and the space a Session needs scales with how long it ran. A container whose writable layer fills up fails the long Sessions and keeps rendering the short ones. - Memory. The encode runs alongside a headless Chromium. On a memory-capped container, a long Session's encode is the first thing the kernel kills, which surfaces as a failed FFmpeg exit rather than an out-of-memory error.
- The Chromium container. A replay that runs for longer than the container's
TIMEOUThas its browser closed underneath it part-way through, and reportsbrowser_crashed. A saturatedPOOL_MAXis not a failed render: the request waits for a free browser, and if none comes free it is answered with a503and retried later, so it costs time rather than the recording.
Session length is the thing to correlate against first: if the failures are all long Sessions, it's disk or memory rather than anything about the Session's content.
Storing Recordings
Recordings can be stored on a local volume (the default), on Amazon S3, on Azure Blob Storage, or on Google Cloud Storage:
| Environment Variable | Description | Default |
|---|---|---|
RECORDINGS_STORAGE | volume stores recordings on disk under /recordings (mount a volume there to persist them) and serves them from {BASE_ENDPOINT}/recordings; s3, azure and gcs upload them to Amazon S3, Azure Blob Storage and Google Cloud Storage respectively. | volume |
RECORDINGS_S3_BUCKET | The S3 bucket recordings are uploaded to when using s3 storage. Falls back to S3_BUCKET. | (nil) |
ACCESS_KEY_ID, SECRET_ACCESS_KEY | Static AWS access keys for s3 storage. Optional: leave both unset to resolve credentials from the environment instead (IRSA, Pod Identity, instance profile), as described below. | (nil) |
AWS_REGION | The AWS region of the S3 bucket. | (nil) |
RECORDINGS_AZURE_CONTAINER | The blob container recordings are uploaded to when using azure storage. Falls back to AZURE_CONTAINER. | (nil) |
AZURE_STORAGE_CONNECTION_STRING | The storage account connection string used for azure storage. | (nil) |
RECORDINGS_GCS_BUCKET | The Cloud Storage bucket recordings are uploaded to when using gcs storage. Falls back to GCS_BUCKET. | (nil) |
GCP_CREDENTIALS_JSON | The service account key JSON used for gcs storage. Optional: leave it unset to use Application Default Credentials, which GKE Workload Identity provides. | (nil) |
RECORDINGS_BASE_URL | The public base URL recording links point to. Set it when using cloud storage (your bucket, container or CDN URL). | {BASE_ENDPOINT}/recordings |
CHROMIUM_ENDPOINT | WebSocket endpoint of the Chromium container used to replay browser Sessions. When unset, only screen sharing Sessions can be recorded. | (nil) |
RENDER_RECORD_ENDPOINT | The URL the Chromium container loads to render the replay. Override it when your BASE_ENDPOINT is not reachable from that container. | {BASE_ENDPOINT}/record |
Recording file URLs are not guessable (they contain a UUID), matching the access model of our cloud storage.
Using Cloud Identity Instead of Static Keys
On AWS, leave ACCESS_KEY_ID and SECRET_ACCESS_KEY unset and the server resolves credentials through the AWS SDK default provider chain: the standard AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY variables, IAM Roles for Service Accounts (IRSA) and EKS Pod Identity, ECS task roles, and EC2 instance profiles. Credentials issued this way are short-lived and rotated by AWS, so no long-lived key is stored in your cluster or handed to the deployment. With IRSA, annotate the service account the Co-Browsing API pod runs under with the role to assume, and set AWS_REGION in the pod spec. EKS usually injects it alongside the IRSA variables, but only when the pod identity webhook is configured with a default region, so do not rely on it.
On GCP, leave GCP_CREDENTIALS_JSON unset to use Application Default Credentials, which is what GKE Workload Identity provides to the pod.
Azure Blob Storage currently requires AZURE_STORAGE_CONNECTION_STRING.
IAM Permissions for S3
Uploading recordings only needs s3:PutObject on the bucket's objects. s3:ListBucket and s3:DeleteObject are used by the visitor deletion endpoints, which remove that visitor's recordings from the bucket; leave them out if you never call those. Recordings are served from RECORDINGS_BASE_URL, so the server never reads them back and needs no s3:GetObject. If the bucket is encrypted with SSE-KMS, the role also needs kms:GenerateDataKey on the key.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UploadRecordings",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::RECORDINGS_BUCKET/*"
},
{
"Sid": "DeleteRecordingsOfDeletedVisitors",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::RECORDINGS_BUCKET", "arn:aws:s3:::RECORDINGS_BUCKET/*"]
}
]
}