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.
Before you start
- Where your InsightRecorder runs
- The base URL every call goes to —
http://localhost:8080when you are running it locally, your own hostname otherwise. - An API key
- Settings → API keys, scoped to
logs:writeandbug: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.
- 01
Install
HTTP is ext-curl, JSON is ext-json; it cannot collide with the Guzzle version you pin.
composer require insightrecorder/insight-recorder - 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(); - 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)); - 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 - 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 upNothing 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.
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.