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:
- BugSnag for Android
- BugSnag for iOS
- BugSnag package for Unity
- BugSnag package for Unreal Engine
- BugSnag integration for Cocos2d-x
BugSink supports the Sentry SDKs:
- Sentry SDK for Android
- Sentry SDK for iOS
- Sentry SDK for Unity
- Sentry SDK for Unreal Engine
- Sentry SDK for Godot Engine
- Sentry SDK for browser JavaScript
DataDog:
GlitchTip:
- GlitchTip for Android
- GlitchTip for Objective-C
- GlitchTip for C#
- GlitchTip for C/C++
- GlitchTip for browser JavaScript
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.
