Skip to content

chore(flags): use new /flags endpoint instead of /decide #345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 16, 2025
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Next

- fix: do not call flags callback with invalid flags ([#355](https://github.com/PostHog/posthog-ios/pull/355))
- use new `/flags` endpoint instead of `/decide` ([#345](https://github.com/PostHog/posthog-ios/pull/345))

## 3.26.2 - 2025-06-03

24 changes: 13 additions & 11 deletions PostHog/PostHogApi.swift
Original file line number Diff line number Diff line change
@@ -192,18 +192,20 @@ class PostHogApi {
}.resume()
}

func decide(
func flags(
distinctId: String,
anonymousId: String?,
groups: [String: String],
completion: @escaping ([String: Any]?, _ error: Error?) -> Void
) {
guard let url = getEndpointURL(
"/decide",
queryItems: URLQueryItem(name: "v", value: "4"),
let url = getEndpointURL(
"/flags",
queryItems: URLQueryItem(name: "v", value: "2"), URLQueryItem(name: "config", value: "true"),
relativeTo: config.host
) else {
hedgeLog("Malformed decide URL error.")
)

guard let url else {
hedgeLog("Malformed flags URL error.")
return completion(nil, nil)
}

@@ -226,34 +228,34 @@ class PostHogApi {
do {
data = try JSONSerialization.data(withJSONObject: toSend)
} catch {
hedgeLog("Error parsing the decide body: \(error)")
hedgeLog("Error parsing the flags body: \(error)")
return completion(nil, error)
}

URLSession(configuration: config).uploadTask(with: request, from: data!) { data, response, error in
if error != nil {
hedgeLog("Error calling the decide API: \(String(describing: error))")
hedgeLog("Error calling the flags API: \(String(describing: error))")
return completion(nil, error)
}

let httpResponse = response as! HTTPURLResponse

if !(200 ... 299 ~= httpResponse.statusCode) {
let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any])
let errorMessage = "Error calling decide API: status: \(httpResponse.statusCode), body: \(jsonBody)."
let errorMessage = "Error calling flags API: status: \(httpResponse.statusCode), body: \(jsonBody)."
hedgeLog(errorMessage)

return completion(nil,
InternalPostHogError(description: errorMessage))
} else {
hedgeLog("Decide called successfully.")
hedgeLog("Flags called successfully.")
}

do {
let jsonData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
completion(jsonData, nil)
} catch {
hedgeLog("Error parsing the decide response: \(error)")
hedgeLog("Error parsing the flags response: \(error)")
completion(nil, error)
}
}.resume()
10 changes: 5 additions & 5 deletions PostHog/PostHogRemoteConfig.swift
Original file line number Diff line number Diff line change
@@ -241,9 +241,9 @@ class PostHogRemoteConfig {
self.loadingFeatureFlags = true
}

api.decide(distinctId: distinctId,
anonymousId: anonymousId,
groups: groups)
api.flags(distinctId: distinctId,
anonymousId: anonymousId,
groups: groups)
{ data, _ in
self.dispatchQueue.async {
// Check for quota limitation first
@@ -260,7 +260,7 @@ class PostHogRemoteConfig {

// Safely handle optional data
guard var data = data else {
hedgeLog("Error: Decide response data is nil")
hedgeLog("Error: Flags response data is nil")
self.notifyFeatureFlagsAndRelease(nil)
return callback(nil)
}
@@ -272,7 +272,7 @@ class PostHogRemoteConfig {
guard let featureFlags = data["featureFlags"] as? [String: Any],
let featureFlagPayloads = data["featureFlagPayloads"] as? [String: Any]
else {
hedgeLog("Error: Decide response missing correct featureFlags format")
hedgeLog("Error: Flags response missing correct featureFlags format")
self.notifyFeatureFlagsAndRelease(nil)
return callback(nil)
}
2 changes: 1 addition & 1 deletion PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
@@ -447,7 +447,7 @@ let maxRetryDelay = 30.0
var props: [String: Any] = ["distinct_id": distinctId]

if !config.reuseAnonymousId {
// We keep the AnonymousId to be used by decide calls and identify to link the previousId
// We keep the AnonymousId to be used by flags calls and identify to link the previousId
storageManager.setAnonymousId(oldDistinctId)
props["$anon_distinct_id"] = oldDistinctId
}
22 changes: 11 additions & 11 deletions PostHogTests/PostHogApiTest.swift
Original file line number Diff line number Diff line change
@@ -45,10 +45,10 @@ enum PostHogApiTests {
#expect(resp.statusCode == 200)
}

func testDecideEndpoint(forHost host: String) async throws {
func testFlagsEndpoint(forHost host: String) async throws {
let sut = getSut(host: host)
let resp = await getApiResponse { completion in
sut.decide(distinctId: "", anonymousId: "", groups: [:]) { data, _ in
sut.flags(distinctId: "", anonymousId: "", groups: [:]) { data, _ in
completion(data)
}
}
@@ -147,41 +147,41 @@ enum PostHogApiTests {
}
}

@Suite("Test decide endpoint with different host paths")
class TestDecideEndpoint: BaseTestSuite {
@Suite("Test flags endpoint with different host paths")
class TestFlagsEndpoint: BaseTestSuite {
@Test("with host containing no path")
func testHostWithNoPath() async throws {
try await testDecideEndpoint(forHost: "http://localhost")
try await testFlagsEndpoint(forHost: "http://localhost")
}

@Test("with host containing no path and trailing slash")
func testHostWithNoPathAndTrailingSlash() async throws {
try await testDecideEndpoint(forHost: "http://localhost/")
try await testFlagsEndpoint(forHost: "http://localhost/")
}

@Test("with host containing path")
func testHostWithPath() async throws {
try await testDecideEndpoint(forHost: "http://localhost/api/v1")
try await testFlagsEndpoint(forHost: "http://localhost/api/v1")
}

@Test("with host containing path and trailing slash")
func testHostWithPathAndTrailingSlash() async throws {
try await testDecideEndpoint(forHost: "http://localhost/api/v1/")
try await testFlagsEndpoint(forHost: "http://localhost/api/v1/")
}

@Test("with host containing port number")
func testHostWithPortNumber() async throws {
try await testDecideEndpoint(forHost: "http://localhost:9000")
try await testFlagsEndpoint(forHost: "http://localhost:9000")
}

@Test("with host containing port number and path")
func testHostWithPortNumberAndPath() async throws {
try await testDecideEndpoint(forHost: "http://localhost:9000/api/v1")
try await testFlagsEndpoint(forHost: "http://localhost:9000/api/v1")
}

@Test("with host containing port number, path and trailing slash")
func testHostWithPortNumberAndTrailingSlash() async throws {
try await testDecideEndpoint(forHost: "http://localhost:9000/api/v1/")
try await testFlagsEndpoint(forHost: "http://localhost:9000/api/v1/")
}
}
}
14 changes: 7 additions & 7 deletions PostHogTests/PostHogSDKTest.swift
Original file line number Diff line number Diff line change
@@ -216,7 +216,7 @@ class PostHogSDKTest: QuickSpec {
it("loads feature flags automatically") {
let sut = self.getSut(preloadFeatureFlags: true)

waitDecideRequest(server)
waitFlagsRequest(server)
expect(sut.isFeatureEnabled("bool-value")) == true

sut.reset()
@@ -226,7 +226,7 @@ class PostHogSDKTest: QuickSpec {
it("send feature flag event for isFeatureEnabled when enabled") {
let sut = self.getSut(preloadFeatureFlags: true, sendFeatureFlagEvent: true)

waitDecideRequest(server)
waitFlagsRequest(server)
expect(sut.isFeatureEnabled("bool-value")) == true

let events = getBatchedEvents(server)
@@ -249,7 +249,7 @@ class PostHogSDKTest: QuickSpec {
it("send feature flag event with variant response for isFeatureEnabled when enabled") {
let sut = self.getSut(preloadFeatureFlags: true, sendFeatureFlagEvent: true)

waitDecideRequest(server)
waitFlagsRequest(server)
expect(sut.isFeatureEnabled("string-value")) == true

let events = getBatchedEvents(server)
@@ -272,7 +272,7 @@ class PostHogSDKTest: QuickSpec {
it("send feature flag event for getFeatureFlag when enabled") {
let sut = self.getSut(preloadFeatureFlags: true, sendFeatureFlagEvent: true)

waitDecideRequest(server)
waitFlagsRequest(server)
expect(sut.getFeatureFlag("bool-value") as? Bool) == true

let events = getBatchedEvents(server)
@@ -304,7 +304,7 @@ class PostHogSDKTest: QuickSpec {

sut.reloadFeatureFlags()

let requests = getDecideRequest(server)
let requests = getFlagsRequest(server)

expect(requests.count) == 1
let request = requests.first
@@ -365,7 +365,7 @@ class PostHogSDKTest: QuickSpec {
let sut = self.getSut()

sut.reloadFeatureFlags()
waitDecideRequest(server)
waitFlagsRequest(server)

sut.capture("event")

@@ -553,7 +553,7 @@ class PostHogSDKTest: QuickSpec {

sut.reset()

waitDecideRequest(server)
waitFlagsRequest(server)
expect(sut.isFeatureEnabled("bool-value")) == true

sut.close()
20 changes: 10 additions & 10 deletions PostHogTests/TestUtils/MockPostHogServer.swift
Original file line number Diff line number Diff line change
@@ -16,9 +16,9 @@ import OHHTTPStubsSwift
class MockPostHogServer {
var batchRequests = [URLRequest]()
var batchExpectation: XCTestExpectation?
var decideExpectation: XCTestExpectation?
var flagsExpectation: XCTestExpectation?
var batchExpectationCount: Int?
var decideRequests = [URLRequest]()
var flagsRequests = [URLRequest]()
var version: Int = 3

func trackBatchRequest(_ request: URLRequest) {
@@ -29,10 +29,10 @@ class MockPostHogServer {
}
}

func trackDecide(_ request: URLRequest) {
decideRequests.append(request)
func trackFlags(_ request: URLRequest) {
flagsRequests.append(request)

decideExpectation?.fulfill()
flagsExpectation?.fulfill()
}

public var errorsWhileComputingFlags = false
@@ -52,7 +52,7 @@ class MockPostHogServer {
init(version: Int = 3) {
self.version = version

stub(condition: pathEndsWith("/decide")) { _ in
stub(condition: pathEndsWith("/flags")) { _ in
if self.quotaLimitFeatureFlags {
return HTTPStubsResponse(
jsonObject: ["quotaLimited": ["feature_flags"]],
@@ -297,8 +297,8 @@ class MockPostHogServer {
HTTPStubs.onStubActivation { request, _, _ in
if request.url?.lastPathComponent == "batch" {
self.trackBatchRequest(request)
} else if request.url?.lastPathComponent == "decide" {
self.trackDecide(request)
} else if request.url?.lastPathComponent == "flags" {
self.trackFlags(request)
}
}
}
@@ -317,9 +317,9 @@ class MockPostHogServer {

func reset(batchCount: Int = 1) {
batchRequests = []
decideRequests = []
flagsRequests = []
batchExpectation = XCTestExpectation(description: "\(batchCount) batch requests to occur")
decideExpectation = XCTestExpectation(description: "1 decide requests to occur")
flagsExpectation = XCTestExpectation(description: "1 flag requests to occur")
batchExpectationCount = batchCount
errorsWhileComputingFlags = false
return500 = false
10 changes: 5 additions & 5 deletions PostHogTests/TestUtils/TestPostHog.swift
Original file line number Diff line number Diff line change
@@ -25,19 +25,19 @@ func getBatchedEvents(_ server: MockPostHogServer, timeout: TimeInterval = 15.0,
return events
}

func waitDecideRequest(_ server: MockPostHogServer) {
let result = XCTWaiter.wait(for: [server.decideExpectation!], timeout: 15)
func waitFlagsRequest(_ server: MockPostHogServer) {
let result = XCTWaiter.wait(for: [server.flagsExpectation!], timeout: 15)

if result != XCTWaiter.Result.completed {
XCTFail("The expected requests never arrived")
}
}

func getDecideRequest(_ server: MockPostHogServer) -> [[String: Any]] {
waitDecideRequest(server)
func getFlagsRequest(_ server: MockPostHogServer) -> [[String: Any]] {
waitFlagsRequest(server)

var requests: [[String: Any]] = []
for request in server.decideRequests.reversed() {
for request in server.flagsRequests.reversed() {
let item = server.parseRequest(request, gzip: false)
requests.append(item!)
}