Documentation

    Session Recording

    AICopy for LLM

    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:

    1. 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.
    2. MongoDB (MONGO_URI must not be disabled): Session events used for the replay are stored there.
    3. 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 VariableDescriptionDefault
    TOKENThe shared secret used to authenticate connections, passed in the endpoint's ?token= query. When unset, authentication is disabled entirely.(nil)
    PORTThe port the container listens on.3000
    POOL_MINThe number of browsers kept warm and ready.1
    POOL_MAXThe maximum number of concurrent browsers. Past this, requests queue.10
    QUEUE_MAX_WAITHow long a request waits for a free browser before failing, in milliseconds.8000
    TIMEOUTThe maximum lifetime of a single browser session, in milliseconds.3600000
    GOTO_TIMEOUTThe navigation timeout for the container's /screenshot and /pdf HTTP endpoints, in milliseconds. Not used for Session replays.10000
    SHUTDOWN_GRACEHow long in-flight work gets to finish after SIGTERM before the process exits, in milliseconds.10000
    LOG_CONNECTIONSLog a line for each new connection from your Co-Browsing API instance. Set to false to disable.true
    STATS_INTERVAL_MSThe interval of the periodic pool and load stats log line, in milliseconds. 0 disables it.60000
    DEBUGDebug log namespaces, e.g. upscope:*.(nil)
    Fonts in Replays
    The image ships metric-compatible substitutes for the fonts most commonly captured in Sessions (Segoe UI, SF Pro, Apple Color Emoji, and others), so replays match what the Visitor saw. If you have a license for the real fonts, mount them at /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 /tmp before 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 TIMEOUT has its browser closed underneath it part-way through, and reports browser_crashed. A saturated POOL_MAX is not a failed render: the request waits for a free browser, and if none comes free it is answered with a 503 and 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 VariableDescriptionDefault
    RECORDINGS_STORAGEvolume 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_BUCKETThe S3 bucket recordings are uploaded to when using s3 storage. Falls back to S3_BUCKET.(nil)
    ACCESS_KEY_ID, SECRET_ACCESS_KEYStatic 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_REGIONThe AWS region of the S3 bucket.(nil)
    RECORDINGS_AZURE_CONTAINERThe blob container recordings are uploaded to when using azure storage. Falls back to AZURE_CONTAINER.(nil)
    AZURE_STORAGE_CONNECTION_STRINGThe storage account connection string used for azure storage.(nil)
    RECORDINGS_GCS_BUCKETThe Cloud Storage bucket recordings are uploaded to when using gcs storage. Falls back to GCS_BUCKET.(nil)
    GCP_CREDENTIALS_JSONThe 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_URLThe public base URL recording links point to. Set it when using cloud storage (your bucket, container or CDN URL).{BASE_ENDPOINT}/recordings
    CHROMIUM_ENDPOINTWebSocket endpoint of the Chromium container used to replay browser Sessions. When unset, only screen sharing Sessions can be recorded.(nil)
    RENDER_RECORD_ENDPOINTThe 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/*"]
        }
      ]
    }