Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
],
"require": {
"php": ">=8.1",
"launchdarkly/server-sdk": "dev-main as 6.8.x-dev",
"launchdarkly/server-sdk": "^6.8",
"open-telemetry/api": "^1.0",
"psr/log": "^1.0|^2.0|^3.0"
},
Expand Down
26 changes: 16 additions & 10 deletions src/LaunchDarkly/OpenTelemetry/TracingHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
* variation index (emitted as an integer primitive).
* - `feature_flag.result.reason.inExperiment` when the evaluation reason
* is part of an experiment. Omitted when false.
* - `feature_flag.set.id` when {@see TracingHookOptions::$environmentId}
* is configured. Only the options-sourced path is supported; a
* per-evaluation path is not, because the PHP Server-Side SDK does not
* currently expose an environment ID on `EvaluationSeriesContext`.
* - `feature_flag.set.id` when an environment ID is available, sourced
* from {@see TracingHookOptions::$environmentId} when configured, or
* otherwise from `EvaluationSeriesContext::$environmentId` (which the
* LaunchDarkly SDK populates from polling-response metadata when
* fetching directly from LaunchDarkly). The configuration path takes
* precedence when both are set.
*
* When no span is active, the hook is a no-op.
*
Expand Down Expand Up @@ -275,12 +277,16 @@ public function afterEvaluation(
$attributes[Attributes::FEATURE_FLAG_RESULT_REASON_IN_EXPERIMENT] = true;
}

// Emit the configured environment ID as `feature_flag.set.id`. The
// options constructor has already trimmed and rejected empty or
// whitespace-only inputs, so a non-null value here is guaranteed to
// be a usable non-empty string.
if ($this->options->environmentId !== null) {
$attributes[Attributes::FEATURE_FLAG_SET_ID] = $this->options->environmentId;
// Emit the environment ID as `feature_flag.set.id`. The
// configuration-supplied value takes precedence over the value
// carried on the EvaluationSeriesContext. The options constructor
// has already validated and trimmed the configuration path; the
// series-context value is provided by the LaunchDarkly SDK (which
// captures it from the X-Ld-Envid response header) and is emitted
// as-is.
$envId = $this->options->environmentId ?? $seriesContext->environmentId;
if ($envId !== null) {
$attributes[Attributes::FEATURE_FLAG_SET_ID] = $envId;
}

$span->addEvent(self::EVENT_NAME, $attributes);
Expand Down
70 changes: 56 additions & 14 deletions tests/TracingHookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@
* Each test brings up a fresh OpenTelemetry `TracerProvider` wired to an
* `InMemoryExporter` via `SimpleSpanProcessor`, runs the hook, then asserts
* against the captured spans/events.
*
* Known gap: a per-evaluation environment ID supplied via
* `EvaluationSeriesContext` is not implemented because the PHP SDK's
* `EvaluationSeriesContext` does not yet carry an environment ID. The
* absence of an emission on that path is covered indirectly by the
* options-driven tests below, which are the only supported source today.
*/
class TracingHookTest extends TestCase
{
Expand All @@ -56,13 +50,17 @@ protected function tearDown(): void
$this->tracerProvider->shutdown();
}

private function seriesContext(string $flagKey = 'my-flag', ?LDContext $ctx = null): EvaluationSeriesContext
{
private function seriesContext(
string $flagKey = 'my-flag',
?LDContext $ctx = null,
?string $environmentId = null,
): EvaluationSeriesContext {
return new EvaluationSeriesContext(
flagKey: $flagKey,
context: $ctx ?? LDContext::create('user-abc'),
defaultValue: false,
method: 'variation',
environmentId: $environmentId,
);
}

Expand All @@ -82,12 +80,17 @@ private function captureEventAttributes(
EvaluationDetail $detail,
?LDContext $ctx = null,
string $flagKey = 'my-flag',
?string $seriesContextEnvironmentId = null,
): array {
$hook = new TracingHook($options, $this->tracer);
$span = $this->tracer->spanBuilder('parent')->startSpan();
$scope = $span->activate();
try {
$hook->afterEvaluation($this->seriesContext($flagKey, $ctx), [], $detail);
$hook->afterEvaluation(
$this->seriesContext($flagKey, $ctx, $seriesContextEnvironmentId),
[],
$detail,
);
} finally {
$scope->detach();
$span->end();
Expand Down Expand Up @@ -357,11 +360,10 @@ public function testAllOptionalAttributesEmittedTogether(): void
// -----------------------------------------------------------------
// feature_flag.set.id.
//
// Only the options-sourced path is supported. The per-evaluation path
// (environment ID via EvaluationSeriesContext) is a known gap
// documented at the top of this file; we verify it by exercising the
// supported path and asserting the absence of the attribute when no
// environmentId is configured.
// Two sources: TracingHookOptions::$environmentId (configuration) and
// EvaluationSeriesContext::$environmentId (per-evaluation, populated by
// the LaunchDarkly SDK from the X-Ld-Envid response header). The
// configuration path takes precedence when both are set.
// -----------------------------------------------------------------

/**
Expand Down Expand Up @@ -418,6 +420,46 @@ public function testEnvironmentIdOmittedWhenEmptyStringDiscardedByOptions(): voi
$this->assertArrayNotHasKey(Attributes::FEATURE_FLAG_SET_ID, $attrs);
}

/**
* When the configuration does not supply an environment ID but the
* EvaluationSeriesContext does, the hook emits the series-context value
* as `feature_flag.set.id`.
*/
public function testEnvironmentIdEmittedFromSeriesContextWhenOptionsAbsent(): void
{
$ctx = LDContext::create('user-abc');
$attrs = $this->captureEventAttributes(
new TracingHookOptions(),
$this->detail(value: true, idx: null, reason: EvaluationReason::off()),
$ctx,
seriesContextEnvironmentId: 'env-from-context',
);

$this->assertSame([
Attributes::FEATURE_FLAG_KEY => 'my-flag',
Attributes::FEATURE_FLAG_PROVIDER_NAME => 'LaunchDarkly',
Attributes::FEATURE_FLAG_CONTEXT_ID => $ctx->getFullyQualifiedKey(),
Attributes::FEATURE_FLAG_SET_ID => 'env-from-context',
], $attrs);
}

/**
* When both the configuration and the EvaluationSeriesContext supply an
* environment ID, the configuration wins.
*/
public function testOptionsEnvironmentIdTakesPrecedenceOverSeriesContext(): void
{
$ctx = LDContext::create('user-abc');
$attrs = $this->captureEventAttributes(
new TracingHookOptions(environmentId: 'env-from-options'),
$this->detail(value: true, idx: null, reason: EvaluationReason::off()),
$ctx,
seriesContextEnvironmentId: 'env-from-context',
);

$this->assertSame('env-from-options', $attrs[Attributes::FEATURE_FLAG_SET_ID]);
}

/**
* Full combo: environmentId configured, includeValue on, a non-null
* variation index, and an experiment-reason. The event carries all seven
Expand Down
Loading