ローカルでsentryを動かす
Webアプリケーションやモバイルアプリなどで発生したエラーは、Sentryに集約してエラーの管理を行う事がよくある。Sentryは設定も簡単で無料枠もあるため、とても導入しやすいと思う。
データ量にもよるけれど、検証環境や開発環境でさえSentryにデータを送信してもいいとさえ思える。ただCIや自動テストで出たエラーまではSentryに流したくないという事もあるだろう。そのような時にSentryのダミーサーバを構築する手段について考え、手順を確認する事にした。
セルフホスティング
Sentryはソースコードも公開されており、セルフホスティングもできるし、Dockerイメージも用意されている。だから Dockerを使ってSentry自体をローカル環境でホスティングする事自体、それほど難しくない。
https://github.com/getsentry/self-hosted にはセルフホスティングするためのファイル一式がある。また、この環境を使うためのドキュメントも公開されている。
手順はドキュメントを参照すると良いが、基本的には docker compose を使用した一般的な構築方法だろう。
MITMProxyでsentryのダミーサーバを書く
セルフホスティングの方法で構築すると、多くのコンテナが起動する事になる。関連するミドルウェアやツールなどのソフトウェアだけでも、egos-tech/smtp、memcached、redis、postgres、pgbouncer、kafka、 clickhouse、seaweedfsといったものを使用するし、それに加えてそれらに付随するコマンドやSentry自身のコンテナもいくつか起動する事になる。
自分達の管理するインフラ上でSentryをホスティングして運用していくのであれば、それらは必要だろう。でもテスト用のダミーサーバだけなのであれば、フルスペックの機能は必要ないことも多く、一部の機能がそれっぽく振る舞ってくれれば良いだけなので、正直そんなにたくさんコンテナを起動させたくないという事もある。
そんな時にはMITMProxyで、一部のAPIだけのダミーサーバを実装してしまう方が楽かもしれない。そこで、ここではSentry APIの簡易なダミーサーバを実装してみる。
import json
import os
import re
from mitmproxy import http
ORG = os.environ.get("SENTRY_ORG", "DUMMYORG")
APP = os.environ.get("SENTRY_APP", "DUMMYAPP")
VERSION = os.environ.get("SENTRY_VERSION", "(^/)+")
def request(flow: http.HTTPFlow):
method = flow.request.method.upper()
path = flow.request.path
if method == "GET" and path == "/api/0/":
flow.response = http.Response.make(
200, content=json.dumps(
{
"url": "http://localhost:8080",
"chunksPerRequest": 10000000,
"maxRequestSize": 10000000,
"hashAlgorithm": "sha1",
"chunkSize": 10000000,
"concurrency": 1,
"version": "0.0.1-dev1",
"dateCreated": "2026-12-31T23:59:59Z",
"environment": "staging",
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "POST" and re.fullmatch(rf"/api/0/projects/{ORG}/{APP}/releases/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
"url": "http://localhost:8080",
"chunksPerRequest": 10000000,
"maxRequestSize": 10000000,
"hashAlgorithm": "sha1",
"chunkSize": 10000000,
"concurrency": 1,
"version": VERSION,
"dateCreated": "2026-12-31T23:59:59Z",
"environment": "staging",
"enabled": True,
"capabilities": { "chunkedUpload": True },
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "GET" and re.fullmatch(f"/api/0/organizations/{ORG}/chunk-upload/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
"url": "http://localhost:8080",
"chunksPerRequest": 10000000,
"maxRequestSize": 10000000,
"hashAlgorithm": "sha1",
"chunkSize": 10000000,
"concurrency": 1,
"version": VERSION,
"dateCreated": "2026-12-31T23:59:59Z",
"environment": "staging",
"enabled": True,
"capabilities": { "chunkedUpload": True },
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "GET" and re.fullmatch(rf"/api/0/organizations/{ORG}/repos/\?cursor=", path):
flow.response = http.Response.make(
200, content=json.dumps(
[
{
"id": "1",
"sha1": "DUMMY",
"name": "DUMMY",
"size": 1000,
"headers": "{\"foo\": \"bar\"}",
"provider": {"id": "1", "name": "baz", "url": ""},
"status": "ok",
"dateCreated": "2026-12-31T23:59:59Z",
},
]
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "POST" and re.fullmatch(f"/api/0/projects/{ORG}/{APP}/releases/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
"url": "http://localhost:8080",
"chunksPerRequest": 10000000,
"maxRequestSize": 10000000,
"hashAlgorithm": "sha1",
"chunkSize": 10000000,
"concurrency": 1,
"version": VERSION,
"dateCreated": "2026-12-31T23:59:59Z",
"environment": "staging",
"enabled": True,
"capabilities": { "chunkedUpload": True },
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "GET" and re.fullmatch(f"/api/0/organizations/{ORG}/releases/{VERSION}/previous-with-commits/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "GET" and re.fullmatch(f"/api/0/projects/{APP}/releases/{VERSION}/previous-with-commits/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "POST" and re.fullmatch(rf"/api/0/projects/{ORG}/{APP}/releases/{VERSION}/files/", path):
flow.response = http.Response.make(
200, content=json.dumps(
[
{
"id": "1",
"sha1": "DUMMY",
"name": "DUMMY",
"size": 1000,
"headers": {},
},
]
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "POST" and re.fullmatch(rf"/api/0/projects/{ORG}/{APP}/releases/{VERSION}/files/\?cursor=", path):
flow.response = http.Response.make(
200, content=json.dumps(
[
{
"id": "1",
"sha1": "DUMMY",
"name": "DUMMY",
"size": 1000,
"headers": {},
},
]
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "GET" and re.fullmatch(rf"/api/0/projects/{ORG}/{APP}/releases/{VERSION}/files/\?cursor=.*", path):
flow.response = http.Response.make(
200, content=json.dumps(
[
{
"id": "1",
"sha1": "DUMMY",
"name": "DUMMY",
"size": 1000,
"headers": {},
},
]
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "PUT" and re.fullmatch(rf"/api/0/projects/{ORG}/{APP}/releases/{VERSION}/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
"version": VERSION,
"dateCreated": "2026-12-31T23:59:59Z",
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "POST" and re.fullmatch(rf"/api/0/organizations/{ORG}/releases/{VERSION}/deploys/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
"environment": "staging",
}
).encode(),
headers={"Content-Type": "application/json"})
return
if method == "PUT" and re.fullmatch(rf"/api/0/organizations/{ORG}/releases/{VERSION}/", path):
flow.response = http.Response.make(
200, content=json.dumps(
{
"url": "http://localhost:8080",
"chunksPerRequest": 10000000,
"maxRequestSize": 10000000,
"hashAlgorithm": "sha1",
"chunkSize": 10000000,
"concurrency": 1,
"version": VERSION,
"dateCreated": "2026-12-31T23:59:59Z",
"environment": "staging",
"enabled": True,
"capabilities": { "chunkedUpload": True },
},
).encode(),
headers={"Content-Type": "application/json"})
return
flow.response = http.Response.make(
404, content=b"{\"message\":\"not found\" }", headers={"Content-Type": "application/json"})
returnこれを起動する。
mitmproxy -s mitm_sentry.pyこれであれば、小さなコンテナ1つだけでSentryのダミーサーバにできる。機能は一部しかサポートしていないけれど、必要に応じてAPIエンドポイントの実装を増やしてあげればよいし、MITMProxyはそういったことがとてもやりやすい。