Resolver Specific Parameters (ID5 Mobile In-App)#58
Conversation
eugenedorfman-optable
left a comment
There was a problem hiding this comment.
Review of the ID5 Mobile In-App changes. Overall a clean, well-tested feature, and the hid= wire encoding is correct: each hint is emitted as its own repeated hid= query item (never comma-joined), which is what the edge expects since it does not split hid on commas.
Inline comments below, tagged major / minor / nit. Two cross-cutting points that are not line-specific:
- README not updated. There is no
targetingcode sample in the README, so the newhidsparameter and the whole ID5 Mobile In-App feature are undocumented for integrators. A short README/demo snippet showing how to pass hints would help. - ObjC + Swift docs. See the inline note on the doc comment: it should actually explain what a hint ID is and which identifier types are valid.
Most important items to resolve before merge are the three major comments (ObjC API break, IDFA casing, and the implicit opt-in for all existing callers).
| */ | ||
| @objc | ||
| func targeting(_ ids: [OptableSDKIdentifier]) throws { | ||
| func targeting(_ ids: [OptableSDKIdentifier], _ hids: [OptableSDKIdentifier]) throws { |
There was a problem hiding this comment.
Major - breaking change for Objective-C integrators. Changing @objc func targeting(_ ids:) to targeting(_:_:) changes the exported selector from targeting:error: to targeting::error:, so every existing ObjC call site breaks at compile time (ObjC has no default args, which is what the :@[] edits in the demo apps are working around), and the bare :: selector is unidiomatic.
Suggest keeping a single-array @objc func targeting(_ ids:) that delegates with hids: [], and adding a separately-named overload, e.g. @objc(targetingWithIds:hids:error:).
|
|
||
| let hids = hids.hids | ||
| .compactMap({ $0.extendedIdentifier }) | ||
| .compactMap({ URLQueryItem(name: "hid", value: $0) }) |
There was a problem hiding this comment.
Major - IDFA is lowercased before it goes out as a hint. enrichIfNeeded injects the system IDFA as an uppercase UUID string, but OptableIdentifierEncoder.idfa(...) lowercases it, so the edge receives hid=a:<lowercase>. Combined with the edge's known MAID-lowercasing before the ID5 call, ID5 ends up with a lowercase IDFA, while the IAB/OpenRTB convention is IDFA uppercase - risking a 204/no-match for the exact in-app case this PR targets.
Worth settling the canonical casing with the edge/ID5 team (preserve IDFA case in the a: encoding, or uppercase MAIDs server-side) before merge.
| var hids = hids ?? [] | ||
|
|
||
| enrichIfNeeded(ids: &ids) | ||
| enrichIfNeeded(ids: &hids) |
There was a problem hiding this comment.
Major - all existing callers are silently opted into hint/ID5 resolution. enrichIfNeeded(ids: &hids) runs even when the caller passed hids: nil, and in EdgeAPI.targeting the bundle/ver/ua/id5_signature params are appended unconditionally. So every current targeting() call will start sending hid=a:<idfa> plus the resolver params, with no opt-out other than skipAdvertisingIdDetection (which also disables the id= enrichment).
If that implicit opt-in is intended edge behavior, fine - otherwise gate hid emission (and arguably the resolver params) on the caller actually supplying hids.
| queryItems.append(URLQueryItem(name: "ua", value: userAgent)) | ||
| } | ||
|
|
||
| if let targeting = storage.getTargeting(), let id5Signature = targeting.targetingData["id5_signature"] as? String { |
There was a problem hiding this comment.
Minor - the id5_signature cache can be clobbered. It is read from the cached targeting response, but _profile (OptableSDK.swift:400) also calls setTargeting, overwriting the cache with a profile response that will not carry id5_signature. So a profile() call between two targeting() calls silently drops the signature and breaks the ID5 refresh chain. Consider persisting the signature under its own storage key.
Also worth confirming the edge returns id5_signature at the top level of the mobile in-app response (vs nested).
| let urlRequest = try sdk.api.targeting(ids: [.emailAddress("12345"), .phoneNumber("54321")]) | ||
|
|
||
| // `id5_signature` is a resolver-specific parameter sourced from the stored targeting data. | ||
| sdk.api.storage.setTargeting(OptableTargeting(optableTargeting: ["id5_signature": "id5-sig-abc123"])) |
There was a problem hiding this comment.
Minor - test writes to shared storage without cleanup. This persists a fake id5_signature into UserDefaults.standard under the same tenant/slug key the integration tests use, and it is never cleared. That can leak the fake signature into a real edge call during integration runs and makes unit tests order-dependent. Add a clearTargeting() in tearDown (or use an isolated config/suite).
| var hids: [OptableIdentifier] { | ||
| filter { | ||
| switch $0 { | ||
| case .ipv6Address(_), .emailAddress(_), .phoneNumber(_), |
There was a problem hiding this comment.
Minor - every .custom case is treated as a hint. puid/idfv prefixes are node-configured, so only specific cN indexes are valid ID5 hints; passing all custom IDs sends publisher PPIDs down the resolver path where unconfigured ones are dropped server-side. Defensible since the SDK cannot know the node config, but worth a doc note on hids that unconfigured custom prefixes are ignored. The rest of the set (e/p/i6/a/g) correctly matches the ID5-in-app supported hints.
| */ | ||
| func targeting(_ ids: [OptableIdentifier]? = nil, completion: @escaping (Result<OptableTargeting, Error>) -> Void) throws { | ||
| try _targeting(ids: ids, completion: completion) | ||
| func targeting(_ ids: [OptableIdentifier]? = nil, _ hids: [OptableIdentifier]? = nil, completion: @escaping (Result<OptableTargeting, Error>) -> Void) throws { |
There was a problem hiding this comment.
Minor - unlabeled _ hids invites arg transposition. Both params are [OptableIdentifier]?, so targeting(a, b) gives no signal which is which; a swap silently sends hints as id= and vice versa. Labeling it (hids:) stays source-compatible for existing single-arg Swift callers. Same applies to the async overload at line 152.
| public extension OptableSDK { | ||
| /** | ||
| targeting(ids?, completion) calls the Optable Sandbox Targeting API and returns key-value targeting data | ||
| targeting(ids?, hids?, completion) calls the Optable Sandbox Targeting API and returns key-value targeting data |
There was a problem hiding this comment.
Minor - doc comment does not explain hids. The summary line was updated, but the body still just says "You may optionally supply identifiers to enrich the request." It does not say what a hint ID is, how hids differs from ids, which identifier types are valid hints (email/phone/ipv6/IDFA/GAID/custom), or that they drive ID5 resolution. Worth expanding here (and on the async and ObjC overloads).
|
|
||
| // MARK: - Equatable | ||
| extension OptableIdentifier: Equatable { | ||
| public static func == (lhs: OptableIdentifier, rhs: OptableIdentifier) -> Bool { |
There was a problem hiding this comment.
Minor - new Equatable changes public == semantics and is unused in this PR. Hashable already provided a synthesized case-based ==; the eid-based == makes e.g. .emailAddress("A@B.com") == .emailAddress("a@b.com") true, a behavior change for any integrator comparing identifiers. It does make == consistent with the eid-based hash(into:), so if that is the intent it is reasonable - but nothing here uses it. Keep only if intentional.
| .compactMap({ $0.extendedIdentifier }) | ||
| .compactMap({ URLQueryItem(name: "id", value: $0) }) | ||
|
|
||
| let hids = hids.hids |
There was a problem hiding this comment.
Nit - shadowing. The parameter, this local, and the .hids filter property are all named hids. Rename the local (e.g. hidQueryItems) for readability.
Closes #57