Skip to content

Commit

Permalink
fix: custom host with a path (#290)
Browse files Browse the repository at this point in the history
* fix: custom host with a path

* fix: test

* fix: tests

* fix: lint
  • Loading branch information
ioannisj authored Feb 4, 2025
1 parent fb53d3b commit e53eb08
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Next

- fix: custom hosts with a path ([#290](https://github.com/PostHog/posthog-ios/pull/290))
- fix: identify macOS when running Mac Catalyst or iOS on Mac ([#287](https://github.com/PostHog/posthog-ios/pull/287))

## 3.19.2 - 2025-01-30
Expand Down
4 changes: 4 additions & 0 deletions PostHog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
DA5B85882CD21CBB00686389 /* AutocaptureEventProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5B85872CD21CBB00686389 /* AutocaptureEventProcessing.swift */; };
DA6B7C0B2D118C4E0024419F /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; };
DA6B7C0C2D118C4E0024419F /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
DA6F24822D4A6CA100CA2777 /* PostHogApiTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6F24812D4A6CA100CA2777 /* PostHogApiTest.swift */; };
DA7185482D07E11200396388 /* input_3.png in Resources */ = {isa = PBXBuildFile; fileRef = DA7185442D07E11200396388 /* input_3.png */; };
DA7185492D07E11200396388 /* input_2.png in Resources */ = {isa = PBXBuildFile; fileRef = DA7185432D07E11200396388 /* input_2.png */; };
DA71854A2D07E11200396388 /* output_1.webp in Resources */ = {isa = PBXBuildFile; fileRef = DA7185452D07E11200396388 /* output_1.webp */; };
Expand Down Expand Up @@ -565,6 +566,7 @@
DA26419A2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAutocaptureEventTracker.swift; sourceTree = "<group>"; };
DA5AA7132CE245CD004EFB99 /* UIApplication+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+.swift"; sourceTree = "<group>"; };
DA5B85872CD21CBB00686389 /* AutocaptureEventProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocaptureEventProcessing.swift; sourceTree = "<group>"; };
DA6F24812D4A6CA100CA2777 /* PostHogApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogApiTest.swift; sourceTree = "<group>"; };
DA7185422D07E11200396388 /* input_1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = input_1.png; sourceTree = "<group>"; };
DA7185432D07E11200396388 /* input_2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = input_2.png; sourceTree = "<group>"; };
DA7185442D07E11200396388 /* input_3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = input_3.png; sourceTree = "<group>"; };
Expand Down Expand Up @@ -930,6 +932,7 @@
690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */,
690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */,
690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */,
DA6F24812D4A6CA100CA2777 /* PostHogApiTest.swift */,
DACF6D5C2CD2F5BC00F14133 /* PostHogAutocaptureIntegrationSpec.swift */,
DA979D7A2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift */,
699C5FEE2C20242A007DB818 /* UUIDTest.swift */,
Expand Down Expand Up @@ -1873,6 +1876,7 @@
3A62647129CAF67B007E8C07 /* PostHogStorageManagerTest.swift in Sources */,
693E977D2C6257F9004B1030 /* ExampleSanitizer.swift in Sources */,
690FF0DF2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift in Sources */,
DA6F24822D4A6CA100CA2777 /* PostHogApiTest.swift in Sources */,
690FF0BD2AEF93F400A0B06B /* PostHogFeatureFlagsTest.swift in Sources */,
690FF0E92AEFD3BD00A0B06B /* PostHogQueueTest.swift in Sources */,
3AE3FB4B2993A68500AFFC18 /* PostHogStorageTest.swift in Sources */,
Expand Down
40 changes: 29 additions & 11 deletions PostHog/PostHogApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,33 @@ class PostHogApi {
return config
}

private func getURL(_ url: URL) -> URLRequest {
private func getURLRequest(_ url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.timeoutInterval = defaultTimeout
return request
}

private func getEndpointURL(
_ endpoint: String,
queryItems: URLQueryItem...,
relativeTo baseUrl: URL
) -> URL? {
guard var components = URLComponents(
url: baseUrl,
resolvingAgainstBaseURL: true
) else {
return nil
}
let path = "\(components.path)/\(endpoint)"
.replacingOccurrences(of: "/+", with: "/", options: .regularExpression)
components.path = path
components.queryItems = queryItems
return components.url
}

func batch(events: [PostHogEvent], completion: @escaping (PostHogBatchUploadInfo) -> Void) {
guard let url = URL(string: "batch", relativeTo: config.host) else {
guard let url = getEndpointURL("/batch", relativeTo: config.host) else {
hedgeLog("Malformed batch URL error.")
return completion(PostHogBatchUploadInfo(statusCode: nil, error: nil))
}
Expand All @@ -47,7 +65,7 @@ class PostHogApi {
headers["Content-Encoding"] = "gzip"
config.httpAdditionalHeaders = headers

let request = getURL(url)
let request = getURLRequest(url)

let toSend: [String: Any] = [
"api_key": self.config.apiKey,
Expand Down Expand Up @@ -93,7 +111,7 @@ class PostHogApi {
}

func snapshot(events: [PostHogEvent], completion: @escaping (PostHogBatchUploadInfo) -> Void) {
guard let url = URL(string: config.snapshotEndpoint, relativeTo: config.host) else {
guard let url = getEndpointURL(config.snapshotEndpoint, relativeTo: config.host) else {
hedgeLog("Malformed snapshot URL error.")
return completion(PostHogBatchUploadInfo(statusCode: nil, error: nil))
}
Expand All @@ -108,7 +126,7 @@ class PostHogApi {
headers["Content-Encoding"] = "gzip"
config.httpAdditionalHeaders = headers

let request = getURL(url)
let request = getURLRequest(url)

let toSend = events.map { $0.toJSON() }

Expand Down Expand Up @@ -160,18 +178,18 @@ class PostHogApi {
groups: [String: String],
completion: @escaping ([String: Any]?, _ error: Error?) -> Void
) {
var urlComps = URLComponents()
urlComps.path = "/decide"
urlComps.queryItems = [URLQueryItem(name: "v", value: "3")]

guard let url = urlComps.url(relativeTo: config.host) else {
guard let url = getEndpointURL(
"/decide",
queryItems: URLQueryItem(name: "v", value: "3"),
relativeTo: config.host
) else {
hedgeLog("Malformed decide URL error.")
return completion(nil, nil)
}

let config = sessionConfig()

let request = getURL(url)
let request = getURLRequest(url)

let toSend: [String: Any] = [
"api_key": self.config.apiKey,
Expand Down
187 changes: 187 additions & 0 deletions PostHogTests/PostHogApiTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//
// PostHogApiTest.swift
// PostHog
//
// Created by Yiannis Josephides on 29/01/2025.
//

import Foundation
import Testing

@testable import PostHog

@Suite(.serialized)
enum PostHogApiTests {
class BaseTestSuite {
var server: MockPostHogServer!

init() {
server = MockPostHogServer()
server.start()
}

deinit {
server.stop()
server = nil
}

func getApiResponse<T>(
apiCall: @escaping (@escaping (T) -> Void) -> Void
) async -> T {
await withCheckedContinuation { continuation in
apiCall { resp in
continuation.resume(returning: resp)
}
}
}

func testSnapshotEndpoint(forHost host: String) async throws {
let sut = getSut(host: host)
let resp = await getApiResponse { completion in
sut.snapshot(events: [], completion: completion)
}

#expect(resp.error == nil)
#expect(resp.statusCode == 200)
}

func testDecideEndpoint(forHost host: String) async throws {
let sut = getSut(host: host)
let resp = await getApiResponse { completion in
sut.decide(distinctId: "", anonymousId: "", groups: [:]) { data, _ in
completion(data)
}
}

#expect(try #require(resp)["errorsWhileComputingFlags"] as! Bool == false)
}

func testBatchEndpoint(forHost host: String) async throws {
let sut = getSut(host: host)
let resp = await getApiResponse { completion in
sut.batch(events: [], completion: completion)
}

#expect(resp.error == nil)
#expect(resp.statusCode == 200)
}

func getSut(host: String) -> PostHogApi {
PostHogApi(PostHogConfig(apiKey: "123", host: host))
}
}

@Suite("Test batch endpoint with different host paths")
class TestBatchEndpoint: BaseTestSuite {
@Test("with host containing no path")
func testHostWithNoPath() async throws {
try await testBatchEndpoint(forHost: "http://localhost")
}

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

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

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

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

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

@Test("with host containing port number, path and trailing slash")
func testHostWithPortNumberAndTrailingSlash() async throws {
try await testBatchEndpoint(forHost: "http://localhost:9000/api/v1/")
}
}

@Suite("Test snapshot endpoint with different host paths")
class TestSnapshotEndpoint: BaseTestSuite {
@Test("with host containing no path")
func testHostWithNoPath() async throws {
try await testSnapshotEndpoint(forHost: "http://localhost")
}

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

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

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

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

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

@Test("with host containing port number, path and trailing slash")
func testHostWithPortNumberAndTrailingSlash() async throws {
try await testSnapshotEndpoint(forHost: "http://localhost:9000/api/v1/")
}
}

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

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

@Test("with host containing path")
func testHostWithPath() async throws {
try await testDecideEndpoint(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/")
}

@Test("with host containing port number")
func testHostWithPortNumber() async throws {
try await testDecideEndpoint(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")
}

@Test("with host containing port number, path and trailing slash")
func testHostWithPortNumberAndTrailingSlash() async throws {
try await testDecideEndpoint(forHost: "http://localhost:9000/api/v1/")
}
}
}
2 changes: 1 addition & 1 deletion PostHogTests/PostHogSessionManagerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Testing
import XCTest

@Suite(.serialized)
enum PostHogSessionManagerTests {
enum PostHogSessionManagerTest {
@Suite("Test session id rotation logic")
struct SessionRotation {
let mockAppLifecycle: MockApplicationLifecyclePublisher
Expand Down
18 changes: 13 additions & 5 deletions PostHogTests/TestUtils/MockPostHogServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class MockPostHogServer {
public var replayVariantName = "myBooleanRecordingFlag"
public var replayVariantValue: Any = true

init(port _: Int = 9001) {
stub(condition: isPath("/decide")) { _ in
init() {
stub(condition: pathEndsWith("/decide")) { _ in
var flags = [
"bool-value": true,
"string-value": "test",
Expand Down Expand Up @@ -90,7 +90,15 @@ class MockPostHogServer {
return HTTPStubsResponse(jsonObject: obj, statusCode: 200, headers: nil)
}

stub(condition: isPath("/batch")) { _ in
stub(condition: pathEndsWith("/batch")) { _ in
if self.return500 {
HTTPStubsResponse(jsonObject: [], statusCode: 500, headers: nil)
} else {
HTTPStubsResponse(jsonObject: ["status": "ok"], statusCode: 200, headers: nil)
}
}

stub(condition: pathEndsWith("/s")) { _ in
if self.return500 {
HTTPStubsResponse(jsonObject: [], statusCode: 500, headers: nil)
} else {
Expand All @@ -99,9 +107,9 @@ class MockPostHogServer {
}

HTTPStubs.onStubActivation { request, _, _ in
if request.url?.path == "/batch" {
if request.url?.lastPathComponent == "batch" {
self.trackBatchRequest(request)
} else if request.url?.path == "/decide" {
} else if request.url?.lastPathComponent == "decide" {
self.trackDecide(request)
}
}
Expand Down

0 comments on commit e53eb08

Please sign in to comment.