hands out one resource class per API area and carries the organization slug; forOrganization() returns a re-scoped copy, and using($url, $token, $organization) returns a client for another account. Clients are immutable: never add a way to change the singleton's server or token at runtime,...
php artisan boost:add-skill wojt-janowski/laravel-glitchtip
Save this content to: AGENTS.md
# wojt-janowski/laravel-glitchtip
## Quick Reference
- **Type**: Laravel package (library), a typed client for the GlitchTip REST API
- **PHP**: 8.4+ with strict_types
- **Laravel**: 13+
- **Namespace**: `WojtJanowski\GlitchTip\`
- **API reference**: https://app.glitchtip.com/api/docs (OpenAPI at `/api/openapi.json`)
## Architecture
- `GlitchTip` is the entry point (singleton, also the `GlitchTip` facade). It
hands out one resource class per API area and carries the organization slug;
`forOrganization()` returns a re-scoped copy, and `using($url, $token,
$organization)` returns a client for another account. Clients are immutable:
never add a way to change the singleton's server or token at runtime,
because apps read several accounts in one request.
- `Support\Connector` wraps Laravel's HTTP client: base URL, bearer token,
JSON, timeouts, retries, and `RequestFailedException` on any non-2xx.
POSTs are never retried.
- `Resources\*` map one method to one endpoint and return DTOs. Paginated
endpoints return `Support\CursorPage<T>`; unpaginated lists return `list<T>`.
- `Data\*` are `final readonly` DTOs. Response DTOs build through
`fromArray()` using `Support\Attributes`, which never throws on missing or
mistyped keys; input objects (`IssueUpdate`, `IssueFilter`, `MonitorInput`,
`AlertInput`) expose `toArray()`/`toQuery()`.
## API Quirks Worth Knowing
- Query lists must be repeated keys (`project=1&project=2`), not PHP brackets.
Always go through `Support\Query::build()`.
- Pagination is Sentry-style: a `Link` header with `rel`, `results` and
`cursor` attributes, plus `X-Hits`.
- Issue updates apply only the fields sent, so `assignedTo: null` unassigns
while an absent key leaves the assignee alone. `IssueUpdate` tracks which
fields were set.
- Bulk issue update and delete with no filter hit every issue in the
organization. `IssueFilter` refuses an empty filter unless built with
`IssueFilter::everything()`.
- Issue `count` is a string in the API, so DTOs cast it.
- `stats_v2` with `interval=1d` returns only the first hour of each day
(seen on 6.2.6). `Stats::series()` asks for hours and sums them per UTC day.
- Release versions can contain `/` and `@`, so every path segment is
`rawurlencode`d.
- Multipart uploads must not call `asJson()`: its Content-Type header
overrides the multipart boundary. `Connector::postFiles()` handles this.
- Chunk uploads go to `file_gzip`, each part gzipped and named by the SHA-1
of its uncompressed content; GlitchTip verifies the name.
- A few endpoints need no token (`/api/settings/`, heartbeats, invitation
lookups, the sentry-wizard hash endpoints); use `getPublic()`,
`postPublic()` or `deletePublic()` for them.
- User endpoints only act on the token's own user; default to `'me'`.
- GlitchTip 6.2 quirks found by the live suite, each worked around in the client:
- The heartbeat endpoint answers in snake_case (`is_up`, `start_check`).
- Updating a monitor with a null `url` returns 500, so send `""`.
- `PUT /users/{id}/` writes every field and returns 500 on a missing or null
`name`, so send the current name.
- Updating an OpenID Connect app requires `serverUrl` every time.
- Updating a project key is a no-op on the server.
- Instances with registration closed only allow inviting existing users.
## API Coverage
`tests/Fixtures/openapi.json` is GlitchTip's spec, and
`tests/Fixtures/coverage.php` maps every operation in it to a client call, an
alias or an exclusion with a reason. `CoverageTest` fails when an operation is
unmapped and checks each call's HTTP method and path. When GlitchTip adds
endpoints, run `composer spec`, then implement or exclude each new operation.
Exclude only what an API token cannot use or what is not management (DSN
ingest, browser widgets, no-ops), and give the reason.
## Conventions
- `declare(strict_types=1)` in all PHP files
- `final` classes (`final readonly` for DTOs and resources) unless extension is intended
- All parameters and return types fully typed; generics documented for PHPStan
- Public constructors and resource methods are consumer contracts: append new
optional parameters at the end, never reorder
- No `env()` outside `config/glitchtip.php`
## Commands
- `composer test -- tests/Feature/ClientTest.php`: run the focused Pest tests while iterating
- `composer analyse`: PHPStan level 6
- `composer format`: Pint
- `composer refactor`: Rector
`tests/Integration` is an opt-in suite against a real server (see the README);
it is skipped unless `GLITCHTIP_INTEGRATION_*` is set. Run it after changing
request bodies or paths, and never commit the token.
Do not run the complete Pest suite for routine local reassurance. Run focused
and neighbouring test files locally and leave the complete suite to CI.
## Delivery
Use one PR per coherent outcome, with small logical commits. Record changes
under `## [Unreleased]` in `CHANGELOG.md`, and tag releases as `vX.Y.Z` once
CI is green.
WojtJanowski\GlitchTip\/api/openapi.json)GlitchTip is the entry point (singleton, also the GlitchTip facade). It
hands out one resource class per API area and carries the organization slug;
forOrganization() returns a re-scoped copy, and using($url, $token, $organization) returns a client for another account. Clients are immutable:
never add a way to change the singleton's server or token at runtime,
because apps read several accounts in one request.Support\Connector wraps Laravel's HTTP client: base URL, bearer token,
JSON, timeouts, retries, and RequestFailedException on any non-2xx.
POSTs are never retried.Resources\* map one method to one endpoint and return DTOs. Paginated
endpoints return Support\CursorPage<T>; unpaginated lists return list<T>.Data\* are final readonly DTOs. Response DTOs build through
fromArray() using Support\Attributes, which never throws on missing or
mistyped keys; input objects (IssueUpdate, IssueFilter, MonitorInput,
AlertInput) expose toArray()/toQuery().project=1&project=2), not PHP brackets.
Always go through Support\Query::build().Link header with rel, results and
cursor attributes, plus X-Hits.assignedTo: null unassigns
while an absent key leaves the assignee alone. IssueUpdate tracks which
fields were set.IssueFilter refuses an empty filter unless built with
IssueFilter::everything().count is a string in the API, so DTOs cast it.stats_v2 with interval=1d returns only the first hour of each day
(seen on 6.2.6). Stats::series() asks for hours and sums them per UTC day./ and @, so every path segment is
rawurlencoded.asJson(): its Content-Type header
overrides the multipart boundary. Connector::postFiles() handles this.file_gzip, each part gzipped and named by the SHA-1
of its uncompressed content; GlitchTip verifies the name./api/settings/, heartbeats, invitation
lookups, the sentry-wizard hash endpoints); use getPublic(),
postPublic() or deletePublic() for them.'me'.is_up, start_check).url returns 500, so send "".PUT /users/{id}/ writes every field and returns 500 on a missing or null
name, so send the current name.serverUrl every time.tests/Fixtures/openapi.json is GlitchTip's spec, and
tests/Fixtures/coverage.php maps every operation in it to a client call, an
alias or an exclusion with a reason. CoverageTest fails when an operation is
unmapped and checks each call's HTTP method and path. When GlitchTip adds
endpoints, run composer spec, then implement or exclude each new operation.
Exclude only what an API token cannot use or what is not management (DSN
ingest, browser widgets, no-ops), and give the reason.
declare(strict_types=1) in all PHP filesfinal classes (final readonly for DTOs and resources) unless extension is intendedenv() outside config/glitchtip.phpcomposer test -- tests/Feature/ClientTest.php: run the focused Pest tests while iteratingcomposer analyse: PHPStan level 6composer format: Pintcomposer refactor: Rectortests/Integration is an opt-in suite against a real server (see the README);
it is skipped unless GLITCHTIP_INTEGRATION_* is set. Run it after changing
request bodies or paths, and never commit the token.
Do not run the complete Pest suite for routine local reassurance. Run focused and neighbouring test files locally and leave the complete suite to CI.
Use one PR per coherent outcome, with small logical commits. Record changes
under ## [Unreleased] in CHANGELOG.md, and tag releases as vX.Y.Z once
CI is green.
How can I help you explore Laravel packages today?