SDKs / PHP / Laravel / Symfony

PHP / Laravel / Symfony

The one SDK that breaks the shape: PHP has no background threads, so it buffers and flushes at end of request — after the response has already reached the browser.

Requires PHP 8.1+Package insightrecorder/insight-recorderSource on GitHub →

Before you start

Where your InsightRecorder runs
The base URL every call goes to — http://localhost:8080 when you are running it locally, your own hostname otherwise.
An API key
Settings → API keys, scoped to logs:write and bug:write. The token (crk_…) is shown exactly once — it is hashed at rest — and is revocable. Do not use a login JWT: it expires within the hour and is not meant for machines.
  1. 01

    Install

    HTTP is ext-curl, JSON is ext-json; it cannot collide with the Guzzle version you pin.

    composer require insightrecorder/insight-recorder
  2. 02

    Register as singletons

    One shipper per request, in a ServiceProvider or services.yaml — never one per log line.

    $client  = new Client(config('insight.base_url'), config('insight.api_key'));
    $shipper = $client->shipper();
  3. 03

    Bridge Monolog

    Which is how Laravel and Symfony both log. Add it alongside the existing handlers.

    use InsightRecorder\Insight\Monolog\InsightHandler;
    
    $logger->pushHandler(new InsightHandler($shipper));
  4. 04

    Report unhandled exceptions

    From Laravel's withExceptions(...) in bootstrap/app.php, or a Symfony kernel.exception listener.

    $reporter = new ErrorReporter($client, shipper: $shipper, service: 'checkout-api');
    $reporter->report($e); // never throws, never masks the original
  5. 05

    Verify it is working

    Ship one line, then flush. PHP has no background threads, so the shipper buffers and flushes at the end of the request — in a one-off script you flush yourself.

    $shipper->send(new LogEntry(
        message: 'insightrecorder smoke test',
        level: 'error',
    ));
    $shipper->flush();

    Then look for the line at /app/logs and the report at /app/bugs.

    If nothing shows up

    Nothing arrived? Check $shipper->dropped(). On PHP-FPM the flush runs after fastcgi_finish_request(), so the user never waits — but long-running workers (Octane, RoadRunner, queues) must call flush() per request or nothing ever ships.

Before you ship

  • On PHP-FPM the shutdown flush runs after fastcgi_finish_request(), so the user never waits for InsightRecorder.
  • Under Octane, RoadRunner or a queue worker, call flush() per request or job — those processes outlive a request.
  • Retry-After is capped at 5s: honouring a literal hour inside a request would hold it until PHP's execution limit kills it.
PROMPT Install the PHP / Laravel / Symfony SDK with an AI agent
Raw .md

Paste this into a coding agent running inside your project, or point the agent at /docs/prompts/php directly. It is self-contained: the agent needs no prior knowledge of InsightRecorder.

# Install and wire the InsightRecorder PHP SDK

Paste this into a coding agent (Claude Code, Cursor, …) running inside your PHP
application's repository. It is self-contained — the agent needs no prior
knowledge of InsightRecorder.

---

You are working in a PHP application (most likely Laravel or Symfony). Install
and wire the InsightRecorder SDK so it ships its logs to InsightRecorder and reports
unhandled exceptions.

**Context you need:**

- InsightRecorder collects application logs and bug reports. The
  `insightrecorder/insight-recorder` package is the supported way for a PHP
  application to talk to it.
- Requires **PHP 8.1+** and has **zero runtime dependencies** — HTTP is
  `ext-curl`, JSON is `ext-json` — so it cannot conflict with the Guzzle
  version this project pins.
- Authentication uses a long-lived **API key** shaped like `crk_…`, created in
  the InsightRecorder UI under *Settings → API keys* with the `logs:write` scope (add
  `bug:write` to report errors). **Do not** use a login JWT: those expire after
  an hour and are not meant for machines.

  Add `bug:read` for anything the SDK reads back (`listBugs/getBug`), and `logs:read` if you query logs. A write-only key is the common mistake: shipping works, the first read returns 403.
- **PHP has no background threads.** The SDK buffers in memory and flushes at
  the end of the request, after `fastcgi_finish_request()` on PHP-FPM, so the
  user never waits. Do not try to make it asynchronous some other way.

- **Wire trace correlation if the application uses OpenTelemetry.** A log line
  and the request that wrote it are one investigation, and that join is the
  reason to ship logs here rather than anywhere else — see the "Trace
  correlation" section of the README for what this language needs (several do it
  automatically; the rest take one hook). Without it, every log call has to carry
  `trace_id` by hand, which nobody sustains past the first week.

**Steps:**

1. `composer require insightrecorder/insight-recorder`.

2. Read configuration from the environment, never hard-code it:
   `INSIGHT_BASE_URL` and `INSIGHT_API_KEY`. Wire them through whatever this
   project already uses — a Laravel `config/` file reading `env()`, Symfony
   parameters, or plain `getenv()`. If either is empty, the application must
   boot normally with shipping disabled: telemetry is never a boot dependency.

3. Register the client and shipper as **singletons** in the container, where
   this project registers its services (a Laravel `ServiceProvider`, Symfony
   `services.yaml`):

   ```php
   $client = new Client(config('insight.base_url'), config('insight.api_key'));
   $shipper = $client->shipper();
   ```

   One shipper per request; do not construct one per log line.

4. Bridge logging through Monolog — which is how both Laravel and Symfony log —
   adding the handler **alongside** the existing ones, never replacing them:

   ```php
   $logger->pushHandler(new InsightHandler($shipper));
   ```

   In Laravel, add it as a channel in `config/logging.php` and include that
   channel in the `stack`. In Symfony, register it as a Monolog handler in
   `config/packages/monolog.yaml`.

5. Report unhandled exceptions with `ErrorReporter`:

   ```php
   $reporter = new ErrorReporter($client, shipper: $shipper, service: '<service name>');
   ```

   Wire it where this project already handles exceptions — Laravel's
   `withExceptions(...)` in `bootstrap/app.php` (or `Handler::report()` on
   Laravel 10 and earlier), or a Symfony `kernel.exception` listener. Report and
   let the exception continue, so the framework's error page still renders.

6. Flushing is automatic at the end of the request. If this project runs
   **Octane, RoadRunner, Swoole, or queue workers**, call `$shipper->flush()`
   at the end of each request or job as well — those processes outlive a single
   request, so the shutdown hook fires far too late.

**Rules — do not violate these:**

- Do **not** write a Monolog handler that makes an HTTP call per log record.
  The SDK batches, caps each request at `batchSize`, retries with backoff, and
  respects the server's quota (429 + `Retry-After`).
- Do **not** call `flush()` inside the request path for every line — that
  defeats the buffering and puts a network round trip on the user's latency.
- Do **not** log the API key, and do not commit it.
- Do **not** capture request bodies, headers, cookies, or query strings in
  reports: they routinely carry credentials and personal data.

**Verify before you finish:**

1. The project's test command passes (`vendor/bin/phpunit`, `php artisan test`).
2. With both env vars set, exercise a route and confirm the log lines appear in
   InsightRecorder at `/app/logs`.
3. Throw a deliberate exception and confirm a bug appears at `/app/bugs`.
4. Confirm the app still boots with both variables **unset**.
5. Report what you changed, and state explicitly where the shipper is flushed.

If anything is ambiguous — which framework version, how logging is configured,
whether the app runs under Octane — inspect the repository and follow what is
already there. Do not restructure the application to fit the SDK.