Connecting Gotcha to Symfony: separate backend and frontend

Gotcha ingests data over the Sentry ingest protocol and over OTLP, so integration means official Sentry SDKs pointed at your instance. Nothing Gotcha-specific ends up in your application: the same SDK you would use with any Sentry-compatible receiver, just with the DSN aimed at your own server.

Let’s cover a practical case rather than a “hello world” one: an app made of two parts — a Symfony API backend and a separate frontend (SPA or classic JS) that live on different domains and talk over an API. That layout adds one wrinkle — cross-origin sending from the browser — which we’ll cover separately.

What we’ll connect

SignalSent fromWhat you need
Errors (exceptions)Symfony backendsentry/sentry-symfony bundle
Tracing (transactions, spans)Symfony backendsame bundle, traces_sample_rate
Web Vitals, front-end errorsbrowser@sentry/browser
Metricsbackend (OTLP)HTTP POST to /v1/metrics
UptimeGotcha, outboundHTTP monitor, no code
AlertsGotchaa channel (Telegram/webhook/email) in the UI

Errors and tracing from the backend, Web Vitals from the frontend, uptime from the outside. Three independent sources, all sending to the same Gotcha project by its DSN.

Where the DSN comes from

After you create a project, Gotcha takes you to the Connect page (/projects/<id>/setup); you can return via the “Connect SDK” button. The DSN looks like this:

https://<public_key>@gotcha.example.com/<project_id>

Both the backend and the browser use the same DSN — the public_key in it is public by design. Keep it in an environment variable: moving to another instance (say, from local to a VPS) becomes a one-line change.

Backend: Symfony

composer require sentry/sentry-symfony

The default Flex recipe enables the bundle only in prod and without tracing. For a self-hosted setup it’s handier to keep it active in every environment but gated on SENTRY_DSN (empty = SDK off, a full no-op), and enable tracing via env. config/packages/sentry.yaml:

sentry:
    dsn: '%env(SENTRY_DSN)%'
    options:
        # A non-zero sample rate turns on performance tracing.
        traces_sample_rate: '%env(float:SENTRY_TRACES_SAMPLE_RATE)%'
        environment: '%kernel.environment%'
        # 404/405 are ordinary web noise (scanners, dead links), not app errors.
        ignore_exceptions:
            - 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'
            - 'Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException'

Register the bundle in every environment (config/bundles.php):

Sentry\SentryBundle\SentryBundle::class => ['all' => true],

And set the variables (.env / production environment):

SENTRY_DSN=https://<public_key>@gotcha.example.com/<project_id>
SENTRY_TRACES_SAMPLE_RATE=0.2   # errors always send; traces on 20% of requests

From there the bundle captures unhandled exceptions on its own. To verify, use a temporary route that throws, or \Sentry\captureMessage('check') — the event shows up under “Issues”.

About ignore_exceptions. Without it, NotFoundHttpException flows into Gotcha as an error, and every scan or dead link clutters your issues. Ignoring client 404/405 leaves only real application errors in the stream.

Frontend: a separate domain and cross-origin sending

The frontend is a separate app on its own domain (say app.example.com), and Gotcha is on gotcha.example.com. The browser SDK will send telemetry to a different origin — the key difference from the backend, which sends server-to-server with no CORS involved.

If the frontend has no bundler (classic JS), build a self-contained @sentry/browser bundle with any bundler, e.g. esbuild:

npm i @sentry/browser
npx esbuild <(echo "export * from '@sentry/browser'") \
  --bundle --minify --format=iife --global-name=Sentry \
  --outfile=public/js/sentry.min.js

Then initialize it (in <head> or at the start of the page scripts):

<script src="/js/sentry.min.js"></script>
<script>
  Sentry.init({
    dsn: "https://<public_key>@gotcha.example.com/<project_id>",
    integrations: [Sentry.browserTracingIntegration()],
    tracesSampleRate: 0.2,
  });
</script>

browserTracingIntegration collects Web Vitals (LCP, CLS, INP, FCP, TTFB) on its own and attaches them to the page-load transaction.

About CORS. The browser on app.example.com POSTs to gotcha.example.com — a cross-origin request. Gotcha’s ingest replies with CORS headers and handles the preflight (OPTIONS), so the browser SDK sends directly, no proxy needed. The DSN (public key) is public, so the receiver allows any origin — just like the Sentry cloud receiver.

If your receiver isn’t reachable by the browser directly for some reason (a strict network perimeter, blocking), the Sentry SDK can send via a tunnel — POST to your own domain, and the backend forwards it to Gotcha. For a typical setup you don’t need it: direct sending is simpler and doesn’t load the backend.

Metrics (optional)

Business metrics go over OTLP: POST to /v1/metrics with the header Authorization: Bearer <public_key> and an OTLP JSON body. From Symfony that’s a one-off console command (schedulable via cron) or any HTTP client. See the metrics documentation.

Uptime: not a single line in the app

Uptime needs no code changes — Gotcha hits the public URL from the outside. In the project: Uptime → New monitor → HTTP, your site’s URL, an interval and fail/recovery thresholds. Incidents, SSL alerts and a public status page come out of the box. See Uptime.

Alerts

Under Alerts, the rules (new issue, regression, spike) are already on; you just add a delivery channel — Telegram, webhook or email (the last needs SMTP). Channels attach to uptime monitors too, so a site outage reaches your notification as well. See Alerts.

Moving to production — one variable

That’s the whole point of env-driven config: both the backend and the browser read the DSN from the environment. Stand up Gotcha on a VPS with a real domain — change SENTRY_DSN (and, for the frontend, the string in Sentry.init), nothing else. The application code stays untouched.

Summary

Next: the documentation, installation and the SDK connection section.