Skip to content

Commit 3770a2d

Browse files
feat(flags): switch local evaluation endpoint to /flags/definitions (#119)
* feat(flags): switch local evaluation endpoint to /flags/definitions * fix(tests): check /flags/definitions before /flags/ in mock client to avoid path collision * fix(tests): use exact paths in mock client and test assertions Match /flags/definitions for local eval and /flags/? for decide endpoint instead of ambiguous prefix matching on /flags/ * fix(tests): use exact decide path /flags/? in assertions to avoid matching /flags/definitions * fix(flags): throw on non-200 responses when loading flag definitions Rust returns 401 as a plain text body without a JSON 'detail' key, unlike Django which returned JSON. Check the HTTP status code first so auth failures are caught regardless of response format. * fix(tests): update test assertions for /flags/definitions path and non-200 error handling - Use exact path matching for decide endpoint (/flags/?) vs definitions (/flags/definitions) - Update ETag test to expect exception on 500 responses (loadFlags now throws on non-200) * fix(flags): log non-200 responses instead of throwing exceptions * fix(flags): set 401 status code in wrong key test
1 parent 5e044fe commit 3770a2d

6 files changed

Lines changed: 50 additions & 44 deletions

File tree

lib/Client.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,12 @@ public function loadFlags()
688688
return;
689689
}
690690

691+
$responseCode = $response->getResponseCode();
692+
if ($responseCode !== 200) {
693+
error_log("[PostHog][Client] Failed to load feature flags (HTTP $responseCode): " . $response->getResponse());
694+
return;
695+
}
696+
691697
$payload = json_decode($response->getResponse(), true);
692698

693699
if ($payload && array_key_exists("detail", $payload)) {
@@ -724,7 +730,7 @@ public function localFlags(): HttpResponse
724730
}
725731

726732
return $this->httpClient->sendRequest(
727-
'/api/feature_flag/local_evaluation?send_cohorts&token=' . $this->apiKey,
733+
'/flags/definitions?send_cohorts&token=' . $this->apiKey,
728734
null,
729735
$headers,
730736
[

test/EtagSupportTest.php

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,8 @@ public function testUpdatesEtagWhen304ResponseIncludesNewEtag(): void
285285

286286
public function testProcessesErrorResponseWithoutFlagsKey(): void
287287
{
288-
// This test verifies current behavior: error responses without a 'flags' key
289-
// will result in empty flags (due to $payload['flags'] ?? [])
290-
// This is pre-existing behavior that's consistent with or without ETag support
288+
// Server error responses (non-200) log and return gracefully,
289+
// preserving previously loaded flags.
291290

292291
$this->http_client = new MockedHttpClient(
293292
host: "app.posthog.com",
@@ -311,15 +310,9 @@ public function testProcessesErrorResponseWithoutFlagsKey(): void
311310
['response' => ['error' => 'Internal Server Error'], 'etag' => null, 'responseCode' => 500]
312311
]);
313312

314-
// loadFlags will parse the response and set featureFlags to []
315-
// This is pre-existing behavior: error responses clear flags
313+
// loadFlags logs the error and returns — flags are preserved from the first load
316314
$this->client->loadFlags();
317-
318-
// Flags are cleared because response doesn't have 'flags' key
319-
// ($payload['flags'] ?? [] evaluates to [])
320-
$this->assertCount(0, $this->client->featureFlags);
321-
322-
// ETag is set to null (from the response)
323-
$this->assertNull($this->client->getFlagsEtag());
315+
$this->assertCount(1, $this->client->featureFlags);
316+
$this->assertEquals('person-flag', $this->client->featureFlags[0]['key']);
324317
}
325318
}

test/FeatureFlagLocalEvaluationTest.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,7 @@ public function testFlagPersonBooleanProperties()
10811081
$this->http_client->calls,
10821082
array(
10831083
0 => array(
1084-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
1084+
"path" => "/flags/definitions?send_cohorts&token=random_key",
10851085
"payload" => null,
10861086
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
10871087
"requestOptions" => array("includeEtag" => true),
@@ -1325,20 +1325,25 @@ public function testLoadFeatureFlags()
13251325

13261326
public function testLoadFeatureFlagsWrongKey()
13271327
{
1328-
self::expectException(Exception::class);
1328+
// Non-200 responses log and return gracefully (no exception).
1329+
// Flags remain empty.
13291330
$this->http_client = new MockedHttpClient(
13301331
host: "app.posthog.com",
1331-
flagEndpointResponse: ["detail" => "Invalid personal API key."]
1332+
flagEndpointResponse: ["detail" => "Invalid personal API key."],
1333+
flagEndpointResponseCode: 401
13321334
);
13331335
$this->client = new Client(
13341336
self::FAKE_API_KEY,
13351337
[
13361338
"debug" => true,
1339+
"host" => "localhost:39876",
13371340
],
13381341
$this->http_client,
13391342
self::FAKE_API_KEY
13401343
);
13411344
PostHog::init(null, null, $this->client);
1345+
1346+
$this->assertEmpty($this->client->featureFlags);
13421347
}
13431348

13441349
public function testSimpleFlag()
@@ -1364,7 +1369,7 @@ public function testSimpleFlag()
13641369
$this->http_client->calls,
13651370
array(
13661371
0 => array(
1367-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
1372+
"path" => "/flags/definitions?send_cohorts&token=random_key",
13681373
"payload" => null,
13691374
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
13701375
"requestOptions" => array("includeEtag" => true),
@@ -1462,7 +1467,7 @@ public function testComputingInactiveFlagLocally()
14621467
$this->http_client->calls,
14631468
array(
14641469
0 => array(
1465-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
1470+
"path" => "/flags/definitions?send_cohorts&token=random_key",
14661471
"payload" => null,
14671472
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
14681473
"requestOptions" => array("includeEtag" => true),
@@ -1497,7 +1502,7 @@ public function testFeatureFlagsLocalEvaluationForCohorts()
14971502
$this->http_client->calls,
14981503
array(
14991504
0 => array(
1500-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
1505+
"path" => "/flags/definitions?send_cohorts&token=random_key",
15011506
"payload" => null,
15021507
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
15031508
"requestOptions" => array("includeEtag" => true),
@@ -1568,7 +1573,7 @@ public function testFeatureFlagsLocalEvaluationForNegatedCohorts()
15681573
$this->http_client->calls,
15691574
array(
15701575
0 => array(
1571-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
1576+
"path" => "/flags/definitions?send_cohorts&token=random_key",
15721577
"payload" => null,
15731578
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
15741579
"requestOptions" => array("includeEtag" => true),

test/FeatureFlagTest.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function testIsFeatureEnabled($response)
6565
$this->http_client->calls,
6666
array(
6767
0 => array(
68-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
68+
"path" => "/flags/definitions?send_cohorts&token=random_key",
6969
"payload" => null,
7070
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
7171
"requestOptions" => array("includeEtag" => true),
@@ -135,7 +135,7 @@ public function testIsFeatureEnabledGroups($response)
135135
$this->http_client->calls,
136136
array(
137137
0 => array(
138-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
138+
"path" => "/flags/definitions?send_cohorts&token=random_key",
139139
"payload" => null,
140140
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
141141
"requestOptions" => array("includeEtag" => true),
@@ -164,7 +164,7 @@ public function testGetFeatureFlag($response)
164164
$this->http_client->calls,
165165
array(
166166
0 => array(
167-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
167+
"path" => "/flags/definitions?send_cohorts&token=random_key",
168168
"payload" => null,
169169
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
170170
"requestOptions" => array("includeEtag" => true),
@@ -231,7 +231,7 @@ public function testGetFeatureFlagGroups($response)
231231
$this->http_client->calls,
232232
array(
233233
0 => array(
234-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
234+
"path" => "/flags/definitions?send_cohorts&token=random_key",
235235
"payload" => null,
236236
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
237237
"requestOptions" => array("includeEtag" => true),
@@ -493,9 +493,9 @@ public function testGetFeatureFlagResultWithLocalEvaluationOnly(): void
493493
$this->assertEquals('simple-flag', $result->getKey());
494494
$this->assertTrue($result->isEnabled());
495495

496-
// Verify no /flags/ network call was made
496+
// Verify no decide (/flags/?v=2) call was made
497497
foreach ($this->http_client->calls as $call) {
498-
$this->assertStringNotContainsString('/flags/', $call['path'], 'Expected no /flags/ call for local evaluation');
498+
$this->assertFalse(str_starts_with($call['path'], '/flags/?'), 'Expected no decide call for local evaluation');
499499
}
500500
}
501501

@@ -527,9 +527,9 @@ public function testGetFeatureFlagResultReturnsNullForLocalEvaluationWhenFlagCan
527527

528528
$this->assertNull($result);
529529

530-
// Verify no /flags/ network call was made
530+
// Verify no decide (/flags/?v=2) call was made
531531
foreach ($this->http_client->calls as $call) {
532-
$this->assertStringNotContainsString('/flags/', $call['path'], 'Expected no /flags/ call for local evaluation only');
532+
$this->assertFalse(str_starts_with($call['path'], '/flags/?'), 'Expected no decide call for local evaluation only');
533533
}
534534
}
535535

@@ -554,7 +554,7 @@ public function testGetFeatureFlagResultWithGroups($response): void
554554
// Verify that groups were passed in the /flags/ request
555555
$flagsCall = null;
556556
foreach ($this->http_client->calls as $call) {
557-
if (str_contains($call['path'], '/flags/')) {
557+
if (str_starts_with($call['path'], '/flags/?')) {
558558
$flagsCall = $call;
559559
break;
560560
}

test/MockedHttpClient.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,8 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders
7575
}
7676
array_push($this->calls, array("path" => $path, "payload" => $payload, "extraHeaders" => $extraHeaders, "requestOptions" => $requestOptions));
7777

78-
if (str_starts_with($path, "/flags/")) {
79-
return new HttpResponse(
80-
json_encode($this->flagsEndpointResponse),
81-
$this->flagsEndpointResponseCode,
82-
null,
83-
$this->flagsEndpointCurlErrno
84-
);
85-
}
86-
87-
if (str_starts_with($path, "/api/feature_flag/local_evaluation")) {
78+
// Local evaluation endpoint: /flags/definitions?...
79+
if (str_starts_with($path, "/flags/definitions")) {
8880
// Check if we have a response queue
8981
if ($this->flagEndpointResponseQueue !== null && !empty($this->flagEndpointResponseQueue)) {
9082
$nextResponse = array_shift($this->flagEndpointResponseQueue);
@@ -112,6 +104,16 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders
112104
);
113105
}
114106

107+
// Decide endpoint: /flags/?v=2
108+
if (str_starts_with($path, "/flags/?")) {
109+
return new HttpResponse(
110+
json_encode($this->flagsEndpointResponse),
111+
$this->flagsEndpointResponseCode,
112+
null,
113+
$this->flagsEndpointCurlErrno
114+
);
115+
}
116+
115117
return parent::sendRequest($path, $payload, $extraHeaders, $requestOptions);
116118
}
117119
}

test/PostHogTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public function testCaptureWithSendFeatureFlagsOption(): void
253253
$this->http_client->calls,
254254
array (
255255
0 => array (
256-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
256+
"path" => "/flags/definitions?send_cohorts&token=random_key",
257257
"payload" => null,
258258
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
259259
"requestOptions" => array("includeEtag" => true),
@@ -309,7 +309,7 @@ public function testCaptureWithLocalSendFlags(): void
309309
$this->http_client->calls,
310310
array (
311311
0 => array (
312-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
312+
"path" => "/flags/definitions?send_cohorts&token=random_key",
313313
"payload" => null,
314314
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
315315
"requestOptions" => array("includeEtag" => true),
@@ -358,7 +358,7 @@ public function testCaptureWithLocalSendFlagsNoOverrides(): void
358358
$this->http_client->calls,
359359
array (
360360
0 => array (
361-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
361+
"path" => "/flags/definitions?send_cohorts&token=random_key",
362362
"payload" => null,
363363
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
364364
"requestOptions" => array("includeEtag" => true),
@@ -537,7 +537,7 @@ public function testDefaultPropertiesGetAddedProperly(): void
537537
$this->http_client->calls,
538538
array(
539539
0 => array(
540-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
540+
"path" => "/flags/definitions?send_cohorts&token=random_key",
541541
"payload" => null,
542542
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
543543
"requestOptions" => array("includeEtag" => true),
@@ -637,7 +637,7 @@ public function testCaptureWithSendFeatureFlagsFalse(): void
637637
$this->http_client->calls,
638638
array (
639639
0 => array (
640-
"path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key",
640+
"path" => "/flags/definitions?send_cohorts&token=random_key",
641641
"payload" => null,
642642
"extraHeaders" => array(0 => 'User-Agent: posthog-php/' . PostHog::VERSION, 1 => 'Authorization: Bearer test'),
643643
"requestOptions" => array("includeEtag" => true),

0 commit comments

Comments
 (0)