dropsolid/langfuse-php-sdk

A PHP SDK for the LangFuse API

Maintainers

Package info

gitlab.com/dropsolid/langfuse-php-sdk

Issues

pkg:composer/dropsolid/langfuse-php-sdk

Transparency log

Statistics

Installs: 46 874

Dependents: 1

Suggesters: 0

Stars: 1

v1.3.0 2026-08-13 11:59 UTC

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 ObservationType values, each sent via its own dedicated ingestion envelope: SPAN/GENERATION/EVENT/EMBEDDING have dedicated factories (createSpan()/createGeneration()/ createEvent()/createEmbedding()), and AGENT/TOOL/CHAIN/ RETRIEVER/EVALUATOR/GUARDRAIL go through createObservation().
  • [x] (v1.3.0) Non-numeric score types (CATEGORICAL/ CORRECTION/TEXT) via Client::createScore()'s $dataType parameter, plus stricter validation for all score types.
  • [x] (v1.3.0) Deterministic trace IDs (Client::trace(id: ...)), Trace.environment/Trace.public, and MetadataCollection/ TagCollection/ScoreData typed DTOs used consistently across Trace/Span/Event/Generation.

Installation

Install via Composer:

composer require dropsolid/langfuse-php-sdk

Requirements

  • PHP 8.2+ (TraceConfig is a readonly class, an 8.2+ feature — composer.json enforces 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 constraintResolvesGuzzle
drupal/core-recommended: ~11.3.011.3.167.15.3
drupal/core-recommended: ^11.311.4.57.15.3
drupal/core: ^11.311.4.57.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:

#ChangeWho it breaksFix
1PHP floor >=8.1>=8.2consumers on 8.1the 8.1 claim was already false — TraceConfig used readonly class since v1.1.0
2Guzzle floor → ^7.15.2Drupal < 11.3.14update core (see above)
3Trace::getScores() returns ScoreData[]$score['value']$score->value or ->toArray()
4toArray()['type'] removedpayload inspectorsgetType() / getObservationType()
510 methods added to 3 interfacesanyone implementing themextend/wrap the concrete classes
6$type protected property removed; $level/$statusMessage/$metadata moved or retypedexternal subclassesset $observationType instead
7Tags trimmed/de-duplicated; empty & non-string rejectedpreviously-silent inputclean tags before passing
8Metadata validated as JSON-encodablepreviously-silent inputpreviously failed later at encode time
9Score values reject NAN/INF; BOOLEAN must be 0/1; TEXT 1–500 charspreviously-silent inputpreviously dropped server-side
10Generation no longer extends Spaninstanceof Span checksmatches v1.2 again — ADR 0003 superseded by 0004
11Unserializing objects written by v1.2consumers persisting SDK objectshandled__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 ObservationType values, 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/ScoreData typed DTOs used consistently across Trace/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 partial HTTP 207 failure, doesn't distinguish which items actually landed before retrying — see AGENTS.md's Known Gaps.
  • [ ] OpenTelemetry ingestion transport - parked for a future v2.x, not v1.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