Error Tracking for Games

When releasing a game for mobile, the web, VR, PC, whether it's a live service or single-player game, error tracking is a must-have. When a new release goes out, you want to know if players are experiencing issues due to that release. You will want to know where the error occurred and if there are commonalities to the players who are experiencing the issue.

Tools for Error Tracking and Application Performance Monitoring

This is where error tracking tools come into play. There are many to choose from and they have different features and price points.

Here are some great options for error tracking tools, in a table for easier comparison. Note that each of the products and services may offer more than just error tracking in its feature set.

Service Can it be self-hosted? Free Tier
Rollbar No Yes, 5,000 occurrences and 1,000 replays per month; 30-day data retention
Datadog No No ongoing free tier; 14-day trial
BugSnag No Yes, 7,500 events and 1M spans per month for one user
Raygun No 14-day trial; paid Crash Reporting starts at $40 per 100,000 errors/month when billed annually
PostHog Yes, self-hosting documentation Yes, 100,000 exceptions per month
GlitchTip Yes, installation instructions Yes, 1,000 events per month, unlimited projects and team members
Highlight, now LaunchDarkly Observability No; Highlight service ended February 28, 2026 Highlight is discontinued; LaunchDarkly Observability has separate pricing
Bugsink Yes, self-hosting is free for non-competing use under the Polyform Shield license Yes, 15,000 events per month for one user

However, not all error tracking tools have official support for some programming languages and frameworks. Not all of them have clear documentation, tutorials and guides. It's important to evaluate error tracking tools based on whether they support the programming languages your team is using, the user interface to view the errors, whether it supports SAML and Single Sign On, and whether the documentation is helpful. For example, PostHog has an official Android SDK but does not have an SDK for iOS although that gap can be filled by capturing errors and exceptions and manually sending them via HTTPS. Self-hosted open source versions of the error tracking service may also differ in the feature set and offer fewer features than the cloud-hosted version.

Implementing Error Tracking

Typically, the SDKs for each error tracking service will provide a way to capture all errors and exceptions and send them to the service, the only requirement being that you create an account and supply an API key. Other parameters such as the version release number and user information can be included when sending data about the error that occurred.

The following is a reference with links to packages for each error tracking tool along with code samples to show how to add them to your project.

BugSnag:

BugSink supports the Sentry SDKs:

DataDog:

GlitchTip:

Highlight / LaunchDarkly:

PostHog:

Raygun:

Rollbar:

Implementing Error Tracking for a Python Server

The following are some code samples for implementing Error

Rollbar for Python

Rollbar can be implemented for FastAPI, Django, Celery, or directly in Python like this:

import os
import rollbar

rollbar.init(os.getenv('ROLLBAR_API_KEY'), os.getenv('SERVER_ENVIRONMENT'), code_version=os.getenv('COMMIT_SHA'))

try:
    main_app_loop()
except IOError:
    rollbar.report_message('Got an IOError in the main loop', 'warning')
except:
    # catch-all
    rollbar.report_exc_info()

BugSnag for Python

BugSnag for standalone Python scripts:

import os
import os.path
import bugsnag

bugsnag.configure(
    api_key=os.getenv("BUGSNAG_API_KEY"),
    project_root=os.path.abspath(os.path.dirname(__file__)),
)

BugSnag for Django applications:

# settings.py
import os
import os.path
import bugsnag

BUGSNAG = {
    "api_key": os.getenv("BUGSNAG_API_KEY"),
    "project_root": os.path.abspath(os.path.join(os.path.dirname(__file__), "..")),
}

# ...

MIDDLEWARE = (
    "bugsnag.django.middleware.BugsnagMiddleware",
)

Implementing Error Tracking in JavaScript for Browser-Based Games

Rollbar for JavaScript and TypeScript

Rollbar can be added to React, Angular, and Vue:

import Rollbar from 'rollbar';

const rollbar = new Rollbar({
  accessToken: 'ROLLBAR_ACCESS_TOKEN',
  environment: 'production',
});

BugSnag for JavaScript

import Bugsnag from '@bugsnag/js';

Bugsnag.start({
  apiKey: 'BUGSNAG_API_KEY',
  onError: (event) => {
    event.setUser('user-id', 'user-email@localhost', 'User Name');
  }
);

Datadog for JavaScript

import { datadogRum } from '@datadog/browser-rum';

datadogRum.init({
  applicationId: 'APP_ID',
  clientToken: 'CLIENT_TOKEN',
  service: 'SERVICE_NAME',
  env: 'ENVIRONMENT_NAME',
  version: '1.0.0',
  trackUserInteractions: true,
  trackResources: true
});

Migrating From Sentry to BugSink or GlitchTip

The advantage of BugSink and GlitchTip are that they are open source and can be self-hosted. In addition, both error tracking services are compatible with Sentry SDKs. That means you can use the Sentry SDK with any language that they support, set the dsn URL endpoint to BugSink or GlitchTip and start tracking.

If you choose to use the cloud version of Bugsink or GlitchTip, here's how their error tracking plans compare to Sentry (as of October 2026), with the number of error events the plan can track per month. Bugsink prices are converted from euros using the ECB reference rate for October 2, 2026 (€1 = $1.1225) and rounded to the nearest dollar:

Service Plan Monthly Price # of Errors Included
Sentry Developer Free 5k
Bugsink Free Free 15k
GlitchTip Free Free 1k
Sentry Team $26 50k
Bugsink 75K $18 75k
GlitchTip Small $15 100k
Bugsink 600K $56 600k
Sentry Business $80 50k
GlitchTip Medium $50 500k
Bugsink 3M $177 3 million
GlitchTip Large $250 3 million
Bugsink 15M $637 15 million
Bugsink 50M $1,446 50 million

This is the example code in Python that will send error traces to BugSink by configuring the Sentry SDK:

import sentry_sdk
import time
 
sentry_sdk.init(
    dsn="http://your_key@localhost:8001/1",
    traces_sample_rate=1.0
)

print("Causing an error")
1 / 0

This is JavaScript/TypeScript/HTML example code that will send errors to GlitchTip, again by configuring the Sentry SDK to use a different dsn URL endpoint value:

<!DOCTYPE html>
<html>
<body>
    <h1>GlitchTip Test</h1>
    <button onclick="throwError()">Throw Error</button>
    <script
      src="https://browser.sentry-cdn.com/10.38.0/bundle.min.js"
  integrity="sha384-/oTG1DxxBD0jT/ShtVYNvpwDmS2j9zVqquIKPRo4LxMLUCnwdUywlKzgEIwS1mEY"
  crossorigin="anonymous"
    ></script>
    <script>
        const GLITCHTIP_DSN = "http://your_key@localhost:8000/1";
        Sentry.init({ dsn: GLITCHTIP_DSN });
        function throwError() { throw new Error("Hello GlitchTip!"); }
    </script>
</body>
</html>

The following is a Docker compose that will run both BugSink and GlitchTip for testing the Python and JavaScript code above:

Show the Docker compose file
x-environment: &default-environment
  DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
  VALKEY_URL: "" # Set to enable Valkey. Ex: redis://valkey:6379
  SECRET_KEY: change_me_to_something_random # best to run openssl rand -hex 32
  EMAIL_URL: consolemail:// # Example smtp://email:password@smtp_url:port https://glitchtip.com/documentation/install#configuration
  GLITCHTIP_DOMAIN: https://glitchtip.example.com # Change this to your domain
  DEFAULT_FROM_EMAIL: email@example.com # Change this to your email
  ENABLE_ADMIN: "False" # Set to True to enable Django Admin at /admin/
  ENABLE_OPENAPI: "False" # Set to True to enable OpenAPI spec at /api/docs
  # PORT: 8000 # If changing, change the web service port too

services:
  # GlitchTip
  # https://glitchtip.com/assets/compose.minimal.yml
  glitchtip_postgres:
    image: postgres:18
    environment:
      POSTGRES_HOST_AUTH_METHOD: "trust"
    restart: unless-stopped
    volumes:
      - pg-data:/var/lib/postgresql
  glitchtip:
    image: glitchtip/glitchtip:6 # Check https://glitchtip.com/blog/ for new major versions
    depends_on:
      - glitchtip_postgres
    ports:
      - "8000:8000"
    environment: *default-environment
    command: ./bin/run-all-in-one.sh # Runs web/worker/migrate all in one process
    restart: unless-stopped
    volumes:
      - uploads:/code/uploads

  # BugSink
  # https://github.com/bugsink/bugsink/blob/main/docker-compose-sample.yaml
  bugsink_db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: bugsinkuser 
      POSTGRES_PASSWORD: your_super_secret_password  # Change this
      POSTGRES_DB: bugsink
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready -h db      
      retries: 5
      start_period: 10s
      interval: 5s
      timeout: 5s

  bugsink:
    image: bugsink/bugsink:2
    depends_on:
      bugsink_db:
        condition: service_healthy
    restart: unless-stopped
    ports:
      - "8001:8000"
    environment:
      SECRET_KEY: django-insecure-RMLYThim9NybWgXiUGat32Aa0Qbgqscf4NPDQuZO2glcZPOiXn  # Change this (and remove django-insecure prefix), e.g. openssl rand -base64 50
      CREATE_SUPERUSER: email:password  # Change this (or remove it and execute 'createsuperuser' against the running container)
      PORT: 8000
      DATABASE_URL: postgresql://bugsinkuser:your_super_secret_password@db:5432/bugsink # Change password to match POSTGRES_PASSWORD above
      BEHIND_HTTPS_PROXY: "false"  # Change this for setups behind a proxy w/ ssl enabled
      BASE_URL: "http://localhost:8000"
    healthcheck:
      test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8000/\").raise_for_status()'"]
      interval: 5s
      timeout: 20s
      retries: 10

volumes:
  db-data:
  pg-data:
  uploads:

If you would rather run them on Kubernetes, the following manifest deploys the same two services into an error-tracking namespace. Each one gets its own PostgreSQL StatefulSet and a Secret for credentials. Replace every change_me value before using it anywhere other than a local test:

Show the Kubernetes manifest
apiVersion: v1
kind: Namespace
metadata:
  name: error-tracking
---
# GlitchTip
# Compose reference: https://glitchtip.com/assets/compose.minimal.yml
apiVersion: v1
kind: Secret
metadata:
  name: glitchtip
  namespace: error-tracking
type: Opaque
stringData:
  POSTGRES_PASSWORD: change_me # Change this
  DATABASE_URL: postgres://postgres:change_me@glitchtip-postgres:5432/postgres # Password must match POSTGRES_PASSWORD
  SECRET_KEY: change_me_to_something_random # best to run openssl rand -hex 32
---
apiVersion: v1
kind: Service
metadata:
  name: glitchtip-postgres
  namespace: error-tracking
spec:
  clusterIP: None
  selector:
    app: glitchtip-postgres
  ports:
    - port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: glitchtip-postgres
  namespace: error-tracking
spec:
  serviceName: glitchtip-postgres
  replicas: 1
  selector:
    matchLabels:
      app: glitchtip-postgres
  template:
    metadata:
      labels:
        app: glitchtip-postgres
    spec:
      containers:
        - name: postgres
          image: postgres:18
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: glitchtip
                  key: POSTGRES_PASSWORD
          volumeMounts:
            - name: pg-data
              mountPath: /var/lib/postgresql
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "postgres"]
            initialDelaySeconds: 5
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: pg-data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: glitchtip-uploads
  namespace: error-tracking
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: glitchtip
  namespace: error-tracking
spec:
  replicas: 1 # The all-in-one script runs web, worker and migrations in one process
  strategy:
    type: Recreate # The uploads volume is ReadWriteOnce
  selector:
    matchLabels:
      app: glitchtip
  template:
    metadata:
      labels:
        app: glitchtip
    spec:
      initContainers:
        - name: wait-for-db
          image: postgres:18
          command: ["sh", "-c", "until pg_isready -h glitchtip-postgres -p 5432; do sleep 2; done"]
      containers:
        - name: glitchtip
          image: glitchtip/glitchtip:6 # Check https://glitchtip.com/blog/ for new major versions
          command: ["./bin/run-all-in-one.sh"]
          ports:
            - containerPort: 8000
          envFrom:
            - secretRef:
                name: glitchtip
          env:
            - name: VALKEY_URL
              value: "" # Set to enable Valkey. Ex: redis://valkey:6379
            - name: EMAIL_URL
              value: consolemail:// # Example smtp://email:password@smtp_url:port https://glitchtip.com/documentation/install#configuration
            - name: GLITCHTIP_DOMAIN
              value: http://localhost:8000 # Change this to your domain
            - name: DEFAULT_FROM_EMAIL
              value: email@example.com # Change this to your email
            - name: ENABLE_ADMIN
              value: "False" # Set to True to enable Django Admin at /admin/
            - name: ENABLE_OPENAPI
              value: "False" # Set to True to enable OpenAPI spec at /api/docs
          volumeMounts:
            - name: uploads
              mountPath: /code/uploads
          readinessProbe:
            tcpSocket:
              port: 8000
            initialDelaySeconds: 15
            periodSeconds: 10
      volumes:
        - name: uploads
          persistentVolumeClaim:
            claimName: glitchtip-uploads
---
apiVersion: v1
kind: Service
metadata:
  name: glitchtip
  namespace: error-tracking
spec:
  selector:
    app: glitchtip
  ports:
    - port: 8000
      targetPort: 8000
---
# BugSink
# Compose reference: https://github.com/bugsink/bugsink/blob/main/docker-compose-sample.yaml
apiVersion: v1
kind: Secret
metadata:
  name: bugsink
  namespace: error-tracking
type: Opaque
stringData:
  POSTGRES_USER: bugsinkuser
  POSTGRES_PASSWORD: your_super_secret_password # Change this
  POSTGRES_DB: bugsink
  DATABASE_URL: postgresql://bugsinkuser:your_super_secret_password@bugsink-db:5432/bugsink # Password must match POSTGRES_PASSWORD
  SECRET_KEY: change-me-to-a-long-random-string-of-at-least-50-characters # Change this; Bugsink refuses to start with a short or django-insecure- key. e.g. openssl rand -base64 50
  CREATE_SUPERUSER: admin@example.com:change_me # Format is email:password. Change this (or remove it and run 'createsuperuser' in the running pod)
---
apiVersion: v1
kind: Service
metadata:
  name: bugsink-db
  namespace: error-tracking
spec:
  clusterIP: None
  selector:
    app: bugsink-db
  ports:
    - port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bugsink-db
  namespace: error-tracking
spec:
  serviceName: bugsink-db
  replicas: 1
  selector:
    matchLabels:
      app: bugsink-db
  template:
    metadata:
      labels:
        app: bugsink-db
    spec:
      containers:
        - name: postgres
          image: postgres:17-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: bugsink
                  key: POSTGRES_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: bugsink
                  key: POSTGRES_PASSWORD
            - name: POSTGRES_DB
              valueFrom:
                secretKeyRef:
                  name: bugsink
                  key: POSTGRES_DB
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata # Subdirectory avoids the volume's lost+found
          volumeMounts:
            - name: db-data
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "bugsinkuser", "-d", "bugsink"]
            initialDelaySeconds: 5
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: db-data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bugsink
  namespace: error-tracking
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bugsink
  template:
    metadata:
      labels:
        app: bugsink
    spec:
      initContainers:
        - name: wait-for-db
          image: postgres:17-alpine
          command: ["sh", "-c", "until pg_isready -h bugsink-db -p 5432; do sleep 2; done"]
      containers:
        - name: bugsink
          image: bugsink/bugsink:2
          ports:
            - containerPort: 8000
          envFrom:
            - secretRef:
                name: bugsink
          env:
            - name: PORT
              value: "8000"
            - name: BEHIND_HTTPS_PROXY
              value: "false" # Change this for setups behind a proxy w/ ssl enabled
            - name: BASE_URL
              value: http://localhost:8001 # Change this to your domain
          readinessProbe:
            tcpSocket:
              port: 8000
            initialDelaySeconds: 15
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: bugsink
  namespace: error-tracking
spec:
  selector:
    app: bugsink
  ports:
    - port: 8000
      targetPort: 8000

Apply it and use kubectl port-forward to reach each service on the same ports as the Docker compose, so the dsn values in the Python and JavaScript examples above work unchanged:

kubectl apply -f error-tracking.yaml
kubectl -n error-tracking port-forward svc/glitchtip 8000:8000
kubectl -n error-tracking port-forward svc/bugsink 8001:8000

BugSink won't start with a short or django-insecure- prefixed SECRET_KEY, and CREATE_SUPERUSER must be in email:password format. The first login for BugSink is the email and password from that value.