From e97f0fc5095e2891feb5f0bcbd595aebc02b7735 Mon Sep 17 00:00:00 2001 From: Christopher Houdlette Date: Mon, 27 Apr 2026 17:35:36 -0600 Subject: [PATCH 1/2] feat: add MetaRouter.Analytics.shared, deprecate client() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes sc-38240. Add a property-style accessor `MetaRouter.Analytics.shared` that returns the same buffered proxy as `client()`. Mark `client()` as deprecated with `@available(*, deprecated, renamed: "shared")` so existing callers get a one-line fix-it warning. Why: - Idiomatic Swift convention. Apple's framework singletons are all property accessors: URLSession.shared, UserDefaults.standard, FileManager.default, NotificationCenter.default. iOS engineers reach for `.shared` first by muscle memory. - Surfaced during the lifecycle MR (sc-36764) review when README snippets defaulted to `MetaRouter.Analytics.shared.openURL(...)`, which didn't compile because `shared` didn't exist. The fix landed as `client()` for consistency, but `shared` is the right end state. - Reduces friction for Segment iOS migrators (Segment uses Analytics.shared at the call-site level). Migration: - Soft deprecation in this minor release. `client()` keeps working but emits a yellow warning at every call site, with auto-fix-it pointing at `shared`. - v2.0 will remove `client()` entirely (separate ticket when major version bump lands). Internal call sites migrated in this PR: - Tests/MetaRouterTests/MetaRouterTests.swift (2 sites) - Tests/MetaRouterTests/MetaRouterIntegrationTests.swift (6 sites) No README changes — the lifecycle MR (sc-36764) currently uses `client()` consistently in its in-flight slice 4 README; that PR will update to `.shared` after this lands as a follow-up. Behavior change: none. Both accessors return the same proxy instance. 418 tests pass; no new deprecation warnings (all internal callers migrated). --- Sources/MetaRouter/MetaRouter.swift | 8 ++++++++ .../MetaRouterTests/MetaRouterIntegrationTests.swift | 12 ++++++------ Tests/MetaRouterTests/MetaRouterTests.swift | 4 ++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Sources/MetaRouter/MetaRouter.swift b/Sources/MetaRouter/MetaRouter.swift index 7203f87..fbdf4cf 100644 --- a/Sources/MetaRouter/MetaRouter.swift +++ b/Sources/MetaRouter/MetaRouter.swift @@ -43,6 +43,14 @@ public enum MetaRouter { } + /// Idiomatic singleton-style accessor matching Apple SDK conventions + /// (`URLSession.shared`, `UserDefaults.standard`, `FileManager.default`). + /// Returns the same buffered proxy `initialize(with:)` returns — calls + /// made before `initialize` are queued and replayed on bind. + public static var shared: AnalyticsInterface { proxy } + + @available(*, deprecated, renamed: "shared", + message: "Use MetaRouter.Analytics.shared. client() will be removed in v2.0.") public static func client() -> AnalyticsInterface { proxy } public static func reset() { diff --git a/Tests/MetaRouterTests/MetaRouterIntegrationTests.swift b/Tests/MetaRouterTests/MetaRouterIntegrationTests.swift index 131bf8b..8a42dde 100644 --- a/Tests/MetaRouterTests/MetaRouterIntegrationTests.swift +++ b/Tests/MetaRouterTests/MetaRouterIntegrationTests.swift @@ -57,7 +57,7 @@ final class MetaRouterIntegrationTests: XCTestCase { let options = TestDataFactory.makeInitOptions() // Get the proxy (before initialization) - let proxy = MetaRouter.Analytics.client() + let proxy = MetaRouter.Analytics.shared // Make calls before initialization (should be queued) proxy.track("queued_event", properties: nil) @@ -83,7 +83,7 @@ final class MetaRouterIntegrationTests: XCTestCase { // Multiple initialize calls should return the same proxy let client1 = MetaRouter.Analytics.initialize(with: options) let client2 = MetaRouter.Analytics.initialize(with: options) - let client3 = MetaRouter.Analytics.client() + let client3 = MetaRouter.Analytics.shared XCTAssertTrue(client1 === client2, "Multiple initialize calls should return same proxy") XCTAssertTrue(client1 === client3, "Client() should return same proxy as initialize") @@ -309,7 +309,7 @@ final class MetaRouterIntegrationTests: XCTestCase { await fulfillment(of: [expectation], timeout: 5.0) // Verify final state is consistent - let client = MetaRouter.Analytics.client() + let client = MetaRouter.Analytics.shared let debugInfo = await client.getDebugInfo() XCTAssertNotNil(debugInfo) } @@ -334,7 +334,7 @@ final class MetaRouterIntegrationTests: XCTestCase { await fulfillment(of: [expectation], timeout: 5.0) // Should be in a valid state after all operations - let finalClient = MetaRouter.Analytics.client() + let finalClient = MetaRouter.Analytics.shared XCTAssertNotNil(finalClient) } @@ -485,7 +485,7 @@ final class MetaRouterIntegrationTests: XCTestCase { func testSetAdvertisingIdWithProxy() async { // Get proxy before initialization - let proxy = MetaRouter.Analytics.client() + let proxy = MetaRouter.Analytics.shared // Set advertising ID before initialization (should be queued) proxy.setAdvertisingId("QUEUED-IDFA") @@ -555,7 +555,7 @@ final class MetaRouterIntegrationTests: XCTestCase { func testClearAdvertisingIdWithProxyIntegration() async { // Get proxy before initialization - let proxy = MetaRouter.Analytics.client() + let proxy = MetaRouter.Analytics.shared // Set advertising ID before initialization (should be queued) proxy.setAdvertisingId("QUEUED-IDFA") diff --git a/Tests/MetaRouterTests/MetaRouterTests.swift b/Tests/MetaRouterTests/MetaRouterTests.swift index 9a16b0a..59dea5d 100644 --- a/Tests/MetaRouterTests/MetaRouterTests.swift +++ b/Tests/MetaRouterTests/MetaRouterTests.swift @@ -40,9 +40,9 @@ final class MetaRouterTests: XCTestCase { func testClientAndProxyAreConnected() { let options = InitOptions(writeKey: "test-key", ingestionHost: "https://test.com") - let initialProxy = MetaRouter.Analytics.client() + let initialProxy = MetaRouter.Analytics.shared let afterInit = MetaRouter.Analytics.initialize(with: options) - let againProxy = MetaRouter.Analytics.client() + let againProxy = MetaRouter.Analytics.shared XCTAssertTrue(initialProxy === afterInit, "Initialize should return same proxy") XCTAssertTrue(afterInit === againProxy, "Client should return same proxy") From 0766ecdc4843dd5db17565536f9fc93ae5200bf8 Mon Sep 17 00:00:00 2001 From: Christopher Houdlette Date: Mon, 27 Apr 2026 17:39:39 -0600 Subject: [PATCH 2/2] docs: add MetaRouter.Analytics.shared to API Reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface the new property-style accessor in the README's API Reference section so it's discoverable. Adds a short subsection between initialize(with:) and Analytics Interface that: - Explains the convention (matches URLSession.shared / UserDefaults .standard / FileManager.default) - Shows the typical usage pattern (initialize once, call .shared anywhere) - Notes that calls before initialize are buffered identically to the proxy returned from initialize(with:) - Calls out that client() is now deprecated and points users at .shared No new TOC entry — matches the existing convention of listing only top-level sections in the TOC, not sub-sections. --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 3a85704..7e4aff7 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,23 @@ Calls to `track`, `identify`, etc. are **buffered in-memory** by the proxy and r - On fatal config errors (`401/403/404`), the client enters **disabled** state and drops subsequent calls. - `sentAt` is stamped when the batch is prepared for transmission (just before network send). If you need the original occurrence time, pass your own `timestamp` on each event. +### MetaRouter.Analytics.shared + +Property-style accessor for the live proxy, matching Apple SDK convention (`URLSession.shared`, `UserDefaults.standard`, etc.). Returns the same proxy that `initialize(with:)` returns — call it from anywhere in your app once the SDK has been initialized: + +```swift +// Initialize once at app launch +MetaRouter.Analytics.initialize(with: options) + +// Use anywhere — no need to thread the proxy through your code +MetaRouter.Analytics.shared.track("Button Tapped") +MetaRouter.Analytics.shared.identify("user123") +``` + +`.shared` is safe to call before `initialize(with:)` — the proxy buffers calls until binding completes (same FIFO + replay-on-ready semantics described above). Use the proxy returned from `initialize(with:)` if you prefer dependency-injection style; both refer to the same underlying instance. + +> **Note:** `MetaRouter.Analytics.client()` is deprecated as of this release; use `.shared` instead. Existing call sites will continue to work (with a yellow deprecation warning) until the next major version. + ### Analytics Interface The analytics client provides the following methods: