dropsolid / langfuse-php-sdk
A PHP SDK for the LangFuse API
Requires
- php: >=8.2
- ext-mbstring: *
- guzzlehttp/guzzle: ^7.15.2
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5.50
- squizlabs/php_codesniffer: ^4.0
This package is not auto-updated.
Last update: 2026-08-18 20:22:52 UTC
README
A PHP Client for LangFuse API
This SDK provides a convenient, object‐oriented way to interact with the LangFuse API from your PHP applications. It supports creating and managing traces, spans, generations, events, and scores, and includes integration tests for validation.
Current Implementation
Last released: v1.3.0. See CHANGELOG.md for the authoritative,
up-to-date list; the bullets below are a snapshot and will drift.
- [x] Basic client initialization (keys, host, etc.)
- [x] Tracing service (traces, spans, generations, scores, events)
- [x] Batch (accumulated) sync of traces
- [x] Comprehensive tests (unit and integration)
- [x] SOLID architecture with dependency injection
- [x] Advanced retry logic with circuit breaker patterns
- [x] PHP 8.2+ support
- [x] Strong typing with DTOs and enhanced error handling
- [x] Per-request timeout override capabilities
- [x] User-Agent headers with SDK identification
- [x] Trace input/output support and
TraceInterface - [x] (v1.3.0) All 10
ObservationTypevalues, each sent via its own dedicated ingestion envelope:SPAN/GENERATION/EVENT/EMBEDDINGhave dedicated factories (createSpan()/createGeneration()/createEvent()/createEmbedding()), andAGENT/TOOL/CHAIN/RETRIEVER/EVALUATOR/GUARDRAILgo throughcreateObservation(). - [x] (v1.3.0) Non-numeric score types (
CATEGORICAL/CORRECTION/TEXT) viaClient::createScore()'s$dataTypeparameter, plus stricter validation for all score types. - [x] (v1.3.0) Deterministic trace IDs (
Client::trace(id: ...)),Trace.environment/Trace.public, andMetadataCollection/TagCollection/ScoreDatatyped DTOs used consistently acrossTrace/Span/Event/Generation.
Installation
Install via Composer:
composer require dropsolid/langfuse-php-sdk
Requirements
- PHP 8.2+ (
TraceConfigis areadonly class, an 8.2+ feature —composer.jsonenforces this) guzzlehttp/guzzle>=7.15.2 (see the conflict note below)
Drupal compatibility
This SDK requires guzzlehttp/guzzle ^7.15.2, because every earlier 7.x
is affected by CVE-2026-69246 (high, host-based-check bypass) plus
several medium advisories, all first fixed in 7.15.2.
It installs cleanly on current Drupal 11 — verified by resolving a
real project (not a symlink) against packages.drupal.org:
| Drupal constraint | Resolves | Guzzle |
|---|---|---|
drupal/core-recommended: ~11.3.0 | 11.3.16 | 7.15.3 |
drupal/core-recommended: ^11.3 | 11.4.5 | 7.15.3 |
drupal/core: ^11.3 | 11.4.5 | 7.15.3 |
If you hit a conflict, your Drupal is out of date, not incompatible.
drupal/core-recommended 11.3.13 and earlier pin
guzzlehttp/guzzle ~7.12.1, which cannot intersect ^7.15.2. Note that
pin is itself affected by the same advisories — composer audit on such
a project reports them against its own Guzzle. Drupal raised the pin in
11.3.14, so the fix is an ordinary security update:
composer update drupal/core-recommended --with-dependencies
Lowering this SDK's floor is deliberately not offered: it would mean requiring a Guzzle with a known high-severity CVE.
Upgrading from 1.2 to 1.3
Most code needs no changes. Client, Trace and the observation
factories keep their signatures. These are the five things that can
actually break a caller — the full detail is in CHANGELOG.md, but this
is the part that requires action.
1. PHP 8.2 and Guzzle 7.15.2 are now required.
composer.json previously claimed PHP >=8.1, but TraceConfig has
used readonly class (8.2+) since v1.1.0 — so 8.1 never actually worked;
the constraint was wrong, not the code. Guzzle ^7.15.2 is a security
floor (CVE-2026-69246). On Drupal, update drupal/core-recommended to
11.3.14+ — see "Drupal compatibility" above.
2. Trace::getScores() returns ScoreData[], not arrays.
// before
foreach ($trace->getScores() as $score) { $value = $score['value']; }
// after
foreach ($trace->getScores() as $score) { $value = $score->value; }
// or, for the previous array shape:
foreach ($trace->getScores() as $score) { $value = $score->toArray()['value']; }
3. toArray() no longer contains a type key. No *Body ingestion
schema ever declared one — it existed only on the envelope, so it was
redundant at best and contradictory at worst.
// before
$observation->toArray()['type']; // 'span'
// after
$observation->getType(); // 'span'
$observation->getObservationType(); // ObservationType::SPAN
4. If you implement the SDK's interfaces, you must add methods.
10 were added across ClientInterface, TraceInterface and
ObservationInterface. If you only call the SDK or type-hint against
its interfaces, nothing changes — extend the concrete classes or wrap
them instead of reimplementing the interfaces.
5. Input that used to pass silently may now throw. Tags are trimmed
and de-duplicated, and empty/non-string tags are rejected. Metadata is
validated as JSON-encodable. Score values reject NAN/INF, and
BOOLEAN requires exactly 0 or 1. Each of these previously produced
either a silent server-side drop or a failure much later during
syncTraces().
See ADR 0001 for why these ship in a minor release.
Full list of breaking changes
Mechanically confirmed: 58 (via roave/backward-compatibility-check
from v1.2.0). Plus these, which signature-comparison tooling cannot
see at all:
| # | Change | Who it breaks | Fix |
|---|---|---|---|
| 1 | PHP floor >=8.1 → >=8.2 | consumers on 8.1 | the 8.1 claim was already false — TraceConfig used readonly class since v1.1.0 |
| 2 | Guzzle floor → ^7.15.2 | Drupal < 11.3.14 | update core (see above) |
| 3 | Trace::getScores() returns ScoreData[] | $score['value'] | $score->value or ->toArray() |
| 4 | toArray()['type'] removed | payload inspectors | getType() / getObservationType() |
| 5 | 10 methods added to 3 interfaces | anyone implementing them | extend/wrap the concrete classes |
| 6 | $type protected property removed; $level/$statusMessage/$metadata moved or retyped | external subclasses | set $observationType instead |
| 7 | Tags trimmed/de-duplicated; empty & non-string rejected | previously-silent input | clean tags before passing |
| 8 | Metadata validated as JSON-encodable | previously-silent input | previously failed later at encode time |
| 9 | Score values reject NAN/INF; BOOLEAN must be 0/1; TEXT 1–500 chars | previously-silent input | previously dropped server-side |
| 10 | Generation no longer extends Span | instanceof Span checks | matches v1.2 again — ADR 0003 superseded by 0004 |
| 11 | Unserializing objects written by v1.2 | consumers persisting SDK objects | handled — __unserialize() coerces legacy shapes |
Item 11 is the one to take seriously if you store SDK objects (Drupal
state, cache, queues): it was a fatal TypeError until fixed, and no
BC-checking tool would have caught it.
For the Drupal langfuse module
Do not use ^1.2. It resolves to >=1.2.0 <2.0.0, so it silently
permits 1.3.x — which is only a problem because this SDK shipped
breaking changes in a minor (ADR 0001). Pin the line explicitly:
// module branch targeting the old SDK
"dropsolid/langfuse-php-sdk": "~1.2.0" // >=1.2.0 <1.3.0
// module branch targeting this SDK
"dropsolid/langfuse-php-sdk": "~1.3.0" // >=1.3.0 <1.4.0
The module's own code touches none of items 3–6 and 10 (verified: no
getScores(), no toArray(), no instanceof Span, and it implements
none of the SDK interfaces), so it is compatible with both lines. The
pin exists to protect sites, not the module — a composer update on
an existing install must not cross SDK minors unattended.
Usage
Basic tracing
use Dropsolid\LangFuse\Client;
use Dropsolid\LangFuse\Enum\ObservationType;
use Dropsolid\LangFuse\Enum\ScoreDataType;
$client = new Client([
'public_key' => getenv('LANGFUSE_PUBLIC_KEY'),
'secret_key' => getenv('LANGFUSE_SECRET_KEY'),
'host' => getenv('LANGFUSE_HOST'),
]);
// A trace is the container for one unit of work (e.g. one request, one
// conversation turn — see "Grouping traces into a session" below for
// multi-turn conversations).
$trace = $client->trace('answer-question', input: ['question' => 'What is Langfuse?']);
$retrieval = $trace->createSpan('retrieve-context');
$retrieval->end(['output' => ['chunks' => 3]]);
$generation = $trace->createGeneration('generate-answer', 'gpt-4', input: [
'messages' => [['role' => 'user', 'content' => 'What is Langfuse?']],
]);
$generation->end(['output' => 'Langfuse is an LLM observability platform.']);
// Scores are accumulated on the trace like observations, not sent
// immediately (Client::createScore() below is the one exception).
$trace->score('relevance', 0.95, 'Directly answers the question', $generation);
$trace->end(['output' => ['answer' => 'Langfuse is an LLM observability platform.']]);
// Nothing above hit the network yet — everything is accumulated in
// memory and sent together as one batch.
$client->syncTraces();
Deterministic trace IDs (for idempotent upserts — calling this twice
with the same id updates the same trace rather than creating a new one):
$trace = $client->trace('answer-question', id: $client->generateUuid());
Other observation types (AGENT/TOOL/CHAIN/RETRIEVER/
EVALUATOR/GUARDRAIL) — no model/usage/cost, matching the official
Python SDK. Every observation type can nest children, and no observation
type is a subtype of another (the hierarchy is flat — see
ADR 0004):
$step = $trace->createObservation(ObservationType::TOOL, 'call-weather-api');
$step->withLevel('WARNING')->withStatusMessage('rate limited, retried');
$step->end(['output' => ['temperature_c' => 18]]);
Embeddings get their own factory, because — like a generation — they can report model, token usage, and cost:
$embedding = $trace->createEmbedding('embed-query', 'text-embedding-3-small', ['dimensions' => 256]);
$embedding->withUsageDetails(['input' => 12]) // non-negative integers
->withCostDetails(['input' => 0.0001]); // finite, non-negative
$embedding->end(['output' => ['vector_len' => 256]]);
Non-numeric scores (CATEGORICAL/CORRECTION/TEXT; omit $dataType
for the default numeric behavior) — sent immediately, not accumulated,
since it scores a trace by ID alone with no live Trace object to attach
to:
$client->createScore(
traceId: $trace->getId(),
name: 'sentiment',
value: 'positive',
dataType: ScoreDataType::CATEGORICAL,
);
Configuration
Configure via environment variables or direct constructor args:
LANGFUSE_PUBLIC_KEY="..."
LANGFUSE_SECRET_KEY="..."
LANGFUSE_HOST="..."
Per-sync timeout override
$client = new Client(['timeout' => 20.0]); // Global default
// Quick sync with short timeout
$client->trace('urgent-operation');
$client->syncTraces(['timeout' => 5.0]);
// Large batch with extended timeout
$client->trace('bulk-operation');
$client->syncTraces(['timeout' => 60.0]);
Grouping traces into a session
A conversation spanning multiple turns should be one trace per turn,
linked by a shared sessionId — not one trace with an array of
messages. Pass the same sessionId on every trace in that
conversation; LangFuse's Sessions view groups them.
$sessionId = $client->generateSessionId();
$client->trace('turn-1', sessionId: $sessionId);
// ...later, same conversation...
$client->trace('turn-2', sessionId: $sessionId);
Security note: always mint a dedicated sessionId with
generateSessionId() (or your own random value). Never reuse an
authentication-relevant identifier — a PHP session ID, an auth token,
a CSRF token — as the sessionId. LangFuse is a third-party
observability system with its own access/retention model, typically
broader than your session store; a leaked sessionId should grant an
attacker nothing. If you must derive it from something request-scoped,
hash it with a site-specific secret first (e.g. an HMAC keyed with your
app's own secret), never pass the raw value through.
Roadmap
🎯 v1.1.0 - Quality & Type Safety ✅ COMPLETED
RELEASE NOTES: All v1.1.0 features successfully implemented and tested! This major update transforms the SDK into an enterprise-ready solution with comprehensive type safety, advanced error handling, and flexible configuration options.
Must-Have (Quality Essentials) - ALL COMPLETE:
- [x] Strong Typing & DTOs - Add typed request/response objects for all operations
- [x] Enhanced Error Handling - Specific exception types with detailed context (ApiError, NetworkError, ValidationError)
- [x] User-Agent Headers - Add proper SDK identification headers
- [x] SOLID Architecture - Dependency injection and interface-based design
- [x] PHP 8.2+ Requirement - Upgrade minimum version for better typing support
Should-Have (Configuration Improvements) - ALL COMPLETE:
- [x] Per-Request Timeout Override - Allow custom timeouts per operation
- [x] Retry Configuration - Configurable retry logic with exponential backoff
- [x] Circuit Breaker Pattern - Automatic failure detection with recovery
🎯 v1.3.0 - LangFuse v3 API Parity ✅ COMPLETED
Ingestion-focused, matching this SDK's actual real-world consumer (the
Drupal langfuse module sends traces/observations/scores — it doesn't
read datasets, prompts, or sessions back). See CHANGELOG.md's
[1.3.0] entry for the authoritative list; ADRs in docs/decisions/
for the reasoning behind each.
- [x] All 10
ObservationTypevalues, each on its own dedicated ingestion envelope - [x] Non-numeric score types (
CATEGORICAL/CORRECTION/TEXT) and stricter score value validation - [x] Deterministic trace IDs,
Trace.environment/Trace.public - [x]
MetadataCollection/TagCollection/ScoreDatatyped DTOs used consistently acrossTrace/Span/Event/Generation
🚀 Next — Ingestion reliability, then OTel transport
Priority order reflects this SDK's actual direction (ingestion correctness/reliability first) rather than read-side platform features:
- [ ] Bounded/resumable batch sync -
syncTraces()currently accumulates unboundedly and, on a partialHTTP 207failure, doesn't distinguish which items actually landed before retrying — see AGENTS.md's Known Gaps. - [ ] OpenTelemetry ingestion transport - parked for a future
v2.x, notv1.x— see ADR 0002. - [ ] CI pipeline - the pre-commit/pre-release checklists in AGENTS.md are manual today.
- [ ] Request/Response Logging - Optional debug logging for troubleshooting
- [ ] Advanced Filtering / Pagination Support - reading trace/observation query results back, lower priority than ingestion for the current real-world consumer
Deprioritized — read/management-side platform features
Not actively planned; the current real-world consumer only ingests data, it doesn't manage these resources. Kept here rather than deleted — still fair game if a consumer actually needs one, see the Backlog note below.
- [ ] Session Management - Create and manage user sessions
- [ ] Dataset Support - Basic dataset creation and item management
- [ ] Prompt Management - Create, retrieve, and version prompts
- [ ] Expanded Model Configuration - Enhanced model definitions and management
🔮 Backlog - Future Considerations
Features we might consider in the future. Community contributions welcome!
These features are inspired by the Python SDK but may never be implemented depending on demand and priorities. If your company finds any of these critical, please open an issue to help us prioritize, or feel free to submit a PR - it's open source after all!
Performance & Async:
- Async support (ReactPHP/Amp) for non-blocking operations
- Connection pooling and memory optimization
- Background processing and batch size configuration
Advanced Configuration:
- Proxy support and custom middleware
- Compression support for large payloads
Extended Features:
- Media management (upload/manage media files)
- Analytics queries and advanced reporting
- Webhook support for incoming Langfuse events
- Data export in various formats
- Multi-project support in single client
Enterprise Features:
- Health check endpoints and metrics collection
- Custom serialization strategies
- Advanced debugging and monitoring tools