From a477a843914ac9070641e031d36514b15d4a7b4d Mon Sep 17 00:00:00 2001 From: Adel Date: Fri, 27 Jun 2025 17:36:37 +0200 Subject: [PATCH 1/8] add dediplicate --- FlagShip/Source/Core/FSExposedFlag.swift | 21 ++++++++- FlagShip/Source/Core/FSVisitor+Flags.swift | 44 +++++++++++++++++++ .../Core/FSVisitor+Reconcilliation.swift | 4 ++ FlagShip/Source/Core/FSVisitor.swift | 31 ++++++++++++- .../Source/Strategy/FSDefaultStrategy.swift | 41 ++++++++++++----- iOS Example/QApp/FSConfigViewController.swift | 5 ++- 6 files changed, 129 insertions(+), 17 deletions(-) diff --git a/FlagShip/Source/Core/FSExposedFlag.swift b/FlagShip/Source/Core/FSExposedFlag.swift index bc8fe728..1a382dc4 100644 --- a/FlagShip/Source/Core/FSExposedFlag.swift +++ b/FlagShip/Source/Core/FSExposedFlag.swift @@ -17,6 +17,10 @@ protocol IFlag { // Get metadata var metadata: FSFlagMetadata { get set } + + // Already activated campaign + + var alreadyActivatedCampaign: Bool? { get } } @objc public class FSExposedFlag: NSObject, IFlag { @@ -32,11 +36,15 @@ protocol IFlag { // Value for flag public private(set) var value: Any? - init(key: String, defaultValue: Any? = nil, metadata: FSFlagMetadata, value: Any?) { + // Already activated campaign + public var alreadyActivatedCampaign: Bool? = true + + init(key: String, defaultValue: Any? = nil, metadata: FSFlagMetadata, value: Any?, alreadyActivatedCampaign: Bool = false) { self.key = key self.defaultValue = defaultValue self.metadata = metadata self.value = value + self.alreadyActivatedCampaign = alreadyActivatedCampaign } init(expoedInfo: [String: Any]) { @@ -44,6 +52,7 @@ protocol IFlag { self.value = expoedInfo["value"] self.defaultValue = expoedInfo["defaultValue"] self.metadata = FSFlagMetadata(metadataDico: expoedInfo["metadata"] as? [String: Any] ?? [:]) + self.alreadyActivatedCampaign = expoedInfo["alreadyActivatedCampaign"] as? Bool ?? false // TODO: Review & test later } /// Dictionary that represent the Exposed Flag @@ -62,6 +71,10 @@ protocol IFlag { result.updateValue(aValue, forKey: "value") } + if let aAlreadyActivatedCampaign = alreadyActivatedCampaign { + result.updateValue(aAlreadyActivatedCampaign, forKey: "alreadyActivatedCampaign") + } + return result } @@ -81,9 +94,13 @@ protocol IFlag { result.updateValue(aValue, forKey: "value") } + if let aAlreadyActivated = alreadyActivatedCampaign { + result.updateValue(aAlreadyActivated, forKey: "alreadyActivatedCampaign") + } + guard let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) else { return nil } - return jsonData.jsonString + return jsonData.prettyPrintedJSONString as String? } } diff --git a/FlagShip/Source/Core/FSVisitor+Flags.swift b/FlagShip/Source/Core/FSVisitor+Flags.swift index e1c50bfd..981d6f3f 100644 --- a/FlagShip/Source/Core/FSVisitor+Flags.swift +++ b/FlagShip/Source/Core/FSVisitor+Flags.swift @@ -8,11 +8,15 @@ import Foundation +let FS_SESSION_VISITOR: TimeInterval = 30.0 // 30 * 60 /// 30 mn + public extension FSVisitor { /// Get FSFlag object /// - Parameter key: key represent key modification /// - Returns: FSFLag object func getFlag(key: String) -> FSFlag { + /// Init the session + self.sessionStartDate = Date() // We dispaly a warning if the flag's status is not fetched if self.fetchStatus != .FETCHED { FlagshipLogManager.Log(level: .ALL, tag: .FLAG, messageToDisplay: FSLogMessage.MESSAGE(self.requiredFetchReason.warningMessage(key, self.visitorId))) @@ -23,6 +27,8 @@ public extension FSVisitor { /// Getting FSFlagCollection /// - Returns: an instance of FSFlagCollection with flags func getFlags() -> FSFlagCollection { + /// Init the session + self.sessionStartDate = Date() var ret: [String: FSFlag] = [:] self.currentFlags.forEach { (key: String, _: FSModification) in @@ -30,6 +36,44 @@ public extension FSVisitor { } return FSFlagCollection(flags: ret) } + + internal func isDeduplicatedFlag(campId: String, varGrpId: String) -> Bool { + var ret = true + let duration = Date().timeIntervalSince(self.sessionStartDate) + + print("the timeinterval duration is \(round(duration))") + + if round(duration) > FS_SESSION_VISITOR { + print("La session a duré plus de 30 minutes ----------") + print("vider les saves et envoyer les activates quant meeme") + + // Clear all saved ids for flags + self.activatedVariations.removeAll() + + /// Register et ordonner l envoie + self.activatedVariations.merge([campId: varGrpId]) { _, new in new } + + ret = false + } else { + print("la session a moins que 30 minutes ===> alors on check avant d envoyer activate") + + if self.activatedVariations.keys.contains(campId) { + if varGrpId == self.activatedVariations[campId] { + /// The activation is already done + print("========== Already activated ============") + ret = true + } + } else { + /// Register et ordonner l envoie + self.activatedVariations.merge([campId: varGrpId]) { _, new in new } + print("========== GO for activation ============") + ret = false + } + } + /// reset the timestamps + self.sessionStartDate = Date() + return ret + } } // We keep FlagMap instead using FSFlagV4 wich is also possible diff --git a/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift b/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift index ac2fef03..e56f2c04 100644 --- a/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift +++ b/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift @@ -15,6 +15,8 @@ public extension FSVisitor { /// - Important: After using this method, you should use Flagship.fetchFlags method to update the visitor informations /// - Requires: Make sure that the experience continuity option is enabled on the flagship platform before using this method @objc func authenticate(visitorId: String) { + /// Init the session + self.sessionStartDate = Date() if configManager.flagshipConfig.mode != .DECISION_API { FlagshipLogManager.Log(level: .ALL, tag: .AUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_AUTHENTICATE) return @@ -27,6 +29,8 @@ public extension FSVisitor { /// Use authenticate methode to go from Logged in session to logged out session @objc func unauthenticate() { + /// Init the session + self.sessionStartDate = Date() if configManager.flagshipConfig.mode != .DECISION_API { FlagshipLogManager.Log(level: .ALL, tag: .UNAUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_AUTHENTICATE) return diff --git a/FlagShip/Source/Core/FSVisitor.swift b/FlagShip/Source/Core/FSVisitor.swift index c5fecbd3..c108bbec 100644 --- a/FlagShip/Source/Core/FSVisitor.swift +++ b/FlagShip/Source/Core/FSVisitor.swift @@ -87,6 +87,12 @@ import UIKit // Score value public internal(set) var emotionScoreAI: String? = nil + + // sesstion timestamps + var sessionStartDate: Date + + // List of activated variations + var activatedVariations: [String: String] = [:] /// campId:varGrpId // Refonte status public internal(set) var fetchStatus: FSFlagStatus = .FETCH_REQUIRED { @@ -129,7 +135,7 @@ import UIKit self.visitorId = FSTools.manageVisitorId(aVisitorId) self.anonymousId = nil } - + // Set the user context self.context = FSContext(aContext, visitorId: aVisitorId) @@ -149,9 +155,14 @@ import UIKit self._onFlagStatusChanged = pOnFlagStatusChanged self._onFlagStatusFetchRequired = pOnFlagStatusFetchRequired self._onFlagStatusFetched = pOnFlagStatusFetched + + // init sessionStartTimestamp + self.sessionStartDate = Date() } @objc public func fetchFlags(onFetchCompleted: @escaping () -> Void) { + /// Init the session + self.sessionStartDate = Date() self.prepareEmotionAI(onCompleted: { score, _ in // Set the score self.emotionScoreAI = score @@ -213,6 +224,8 @@ import UIKit } public func collectEmotionsAIEvents(window: UIWindow?, screenName: String? = nil, usingSwizzling: Bool = false) { + /// Init the session + self.sessionStartDate = Date() if Flagship.sharedInstance.eaiCollectEnabled == true { self.strategy?.getStrategy().collectEmotionsAIEvents(window: window, screenName: screenName, usingSwizzling: usingSwizzling) } else { @@ -231,6 +244,8 @@ import UIKit // Update Context // - Parameter newContext: user's context @objc public func updateContext(_ context: [String: Any]) { + /// Init the session + self.sessionStartDate = Date() self._updateContext(context) } @@ -239,6 +254,8 @@ import UIKit // - key: key for the given value // - newValue: value for teh given key public func updateContext(_ key: String, _ newValue: Any) { + /// Init the session + self.sessionStartDate = Date() self._updateContext([key: newValue]) } @@ -247,6 +264,8 @@ import UIKit // - presetKey: name of the preset context, see PresetContext // - newValue: the value for the given key public func updateContext(_ flagshipContext: FlagshipContext, _ value: Any) { + /// Init the session + self.sessionStartDate = Date() /// Check the validity value if !flagshipContext.chekcValidity(value) { FlagshipLogManager.Log(level: .ALL, tag: .UPDATE_CONTEXT, messageToDisplay: FSLogMessage.UPDATE_PRE_CONTEXT_FAILED(flagshipContext.rawValue)) @@ -266,17 +285,24 @@ import UIKit // Get the current context // - Returns: Dictionary that represent a user context @objc public func getContext() -> [String: Any] { + /// Init the session + self.sessionStartDate = Date() return self.context.getCurrentContext() } // Clear the current context @objc public func clearContext() { + /// Init the session + self.sessionStartDate = Date() self.context.clearContext() } // Send Hits // - Parameter T: Hit object public func sendHit(_ event: T) { + /// Init the session + self.sessionStartDate = Date() + self.strategy?.getStrategy().sendHit(event) } @@ -285,6 +311,9 @@ import UIKit // Set the conssent // - Parameter newValue: if true, then flush all stored visitor data @objc public func setConsent(hasConsented: Bool) { + /// Init the session + self.sessionStartDate = Date() + self.hasConsented = hasConsented self.strategy?.getStrategy().setConsent(newValue: hasConsented) diff --git a/FlagShip/Source/Strategy/FSDefaultStrategy.swift b/FlagShip/Source/Strategy/FSDefaultStrategy.swift index ac33b124..5ae498ed 100644 --- a/FlagShip/Source/Strategy/FSDefaultStrategy.swift +++ b/FlagShip/Source/Strategy/FSDefaultStrategy.swift @@ -45,6 +45,11 @@ class FSDefaultStrategy: FSDelegateStrategy { /// Activate Flag func activateFlag(_ flag: FSFlag) { +// if visitor.isDeduplicatedFlag(campId: flag.metadata().campaignId, varGrpId: flag.metadata().variationGroupId) { +// // Skip the activate +// print("Skip the activation ..... the variation is already activated") +// return +// } if let aModification = visitor.currentFlags[flag.key] { // Define Exposed flag and exposed visitor var exposedFlag, exposedVisitor: String? @@ -56,21 +61,33 @@ class FSDefaultStrategy: FSDelegateStrategy { } let activateToSend = Activate(visitor.visitorId, visitor.anonymousId, modification: aModification, exposedFlag, exposedVisitor) - visitor.configManager.trackingManager?.sendActivate(activateToSend, onCompletion: { error, exposedInfosArray in + + /// If dediplicate + if visitor.isDeduplicatedFlag(campId: flag.metadata().campaignId, varGrpId: flag.metadata().variationGroupId) { + // Skip the activate + print("Skip the activation ..... the variation is already activated") + /// Is callback is defined ===> Trigger it + if let aOnVisitorExposed = visitor.configManager.flagshipConfig.onVisitorExposed { + aOnVisitorExposed(FSVisitorExposed(id: visitor.visitorId, anonymousId: visitor.anonymousId, context: visitor.getContext()), FSExposedFlag(key: flag.key, defaultValue: flag.defaultValue, metadata: flag.metadata(), value: flag.value(defaultValue: flag.defaultValue, visitorExposed: false), alreadyActivatedCampaign: true)) + } - if error == nil { - /// Is callback is defined ===> Trigger it - if let aOnVisitorExposed = self.visitor.configManager.flagshipConfig.onVisitorExposed { - exposedInfosArray?.forEach { item in - aOnVisitorExposed(item.visitorExposed, item.exposedFlag) + } else { + visitor.configManager.trackingManager?.sendActivate(activateToSend, onCompletion: { error, exposedInfosArray in + + if error == nil { + /// Is callback is defined ===> Trigger it + if let aOnVisitorExposed = self.visitor.configManager.flagshipConfig.onVisitorExposed { + exposedInfosArray?.forEach { item in + aOnVisitorExposed(item.visitorExposed, item.exposedFlag) + } } + } else { + // The flag error } - } else { - // The flag error - } - }) - // Troubleshooitng activate - FSDataUsageTracking.sharedInstance.processTSHits(label: CriticalPoints.VISITOR_SEND_ACTIVATE.rawValue, visitor: visitor, hit: activateToSend) + }) + // Troubleshooitng activate + FSDataUsageTracking.sharedInstance.processTSHits(label: CriticalPoints.VISITOR_SEND_ACTIVATE.rawValue, visitor: visitor, hit: activateToSend) + } } } diff --git a/iOS Example/QApp/FSConfigViewController.swift b/iOS Example/QApp/FSConfigViewController.swift index cf7d9157..cfc857e2 100644 --- a/iOS Example/QApp/FSConfigViewController.swift +++ b/iOS Example/QApp/FSConfigViewController.swift @@ -105,8 +105,9 @@ class FSConfigViewController: UIViewController, UITextFieldDelegate, FSJsonEdito } }.withLogLevel(.ALL).withOnVisitorExposed { visitorExposed, fromFlag in - print("------- On visitor exposed callback ----------") - print(visitorExposed.toJson()) + //print("------- On visitor exposed callback ----------") + // print(visitorExposed.toJson()) + print(fromFlag.toJson()) print("------- On visitor exposed callback ----------") } From 4d66c5fbcae824bf1743358ce3eaf389264d04e3 Mon Sep 17 00:00:00 2001 From: Adel Date: Mon, 30 Jun 2025 17:37:30 +0200 Subject: [PATCH 2/8] add unit tests --- FlagShip/FlagShipTests/FSOnExposureTest.swift | 25 ++++++++++++++- .../Source/Strategy/FSDefaultStrategy.swift | 31 +++++++++++-------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/FlagShip/FlagShipTests/FSOnExposureTest.swift b/FlagShip/FlagShipTests/FSOnExposureTest.swift index 46fbbae6..e8b3384b 100644 --- a/FlagShip/FlagShipTests/FSOnExposureTest.swift +++ b/FlagShip/FlagShipTests/FSOnExposureTest.swift @@ -78,11 +78,13 @@ final class FSOnExposureTest: XCTestCase { let variation = FSVariation(idVariation: "varId", variationName: "varName", nil, isReference: false) let modif = FSModification(aCampaign: camp, aVariation: variation, valueForFlag: "flagVlaue") let mettdata = FSFlagMetadata(modif) - let flagTest = FSExposedFlag(key: "keyFlag", defaultValue: "dfl", metadata: mettdata, value: "flagVlaue") + let flagTest = FSExposedFlag(key: "keyFlag", defaultValue: "dfl", metadata: mettdata, value: "flagVlaue", alreadyActivatedCampaign: true) XCTAssertTrue(flagTest.toDictionary()["value"] as? String == "flagVlaue") XCTAssertTrue(flagTest.toDictionary()["key"] as? String == "keyFlag") XCTAssertTrue(flagTest.metadata.campaignId == "campId") + XCTAssertTrue(flagTest.toDictionary()["alreadyActivatedCampaign"] as? Bool == true) + XCTAssertTrue(flagTest.toJson()?.count ?? 0 > 0) } @@ -94,4 +96,25 @@ final class FSOnExposureTest: XCTestCase { XCTAssertTrue((visitorObject.toDictionary()["context"] as? [String: Any])?["key1"] as? String == "val1") XCTAssertTrue(visitorObject.toJson()?.count ?? 0 > 0) } + + func testDeduplication() { + let expectationSync = XCTestExpectation(description: "Service-deduplication") + + testVisitor?.fetchFlags(onFetchCompleted: { + if let flag = self.testVisitor?.getFlag(key: "btnTitle") { + XCTAssertTrue(flag.value(defaultValue: "dfl") == "Alpha_demoApp") + // Activate again + XCTAssertTrue(flag.value(defaultValue: "dfl") == "Alpha_demoApp") + + // Check deduplication + XCTAssertTrue(self.testVisitor?.isDeduplicatedFlag(campId: "bvcdqksmicqghldq9agg", varGrpId: "bvcdqksmicqghldq9ahg") ?? false) + // Send hit to reset timer + self.testVisitor?.sendHit(FSScreen("testLocatoin")) + XCTAssertTrue(self.testVisitor?.isDeduplicatedFlag(campId: "bvcdqksmicqghldq9agg", varGrpId: "bvcdqksmicqghldq9ahg") ?? false) + } + expectationSync.fulfill() + + }) + wait(for: [expectationSync], timeout: 5.0) + } } diff --git a/FlagShip/Source/Strategy/FSDefaultStrategy.swift b/FlagShip/Source/Strategy/FSDefaultStrategy.swift index 5ae498ed..0aa369d7 100644 --- a/FlagShip/Source/Strategy/FSDefaultStrategy.swift +++ b/FlagShip/Source/Strategy/FSDefaultStrategy.swift @@ -45,32 +45,37 @@ class FSDefaultStrategy: FSDelegateStrategy { /// Activate Flag func activateFlag(_ flag: FSFlag) { -// if visitor.isDeduplicatedFlag(campId: flag.metadata().campaignId, varGrpId: flag.metadata().variationGroupId) { -// // Skip the activate -// print("Skip the activation ..... the variation is already activated") -// return -// } if let aModification = visitor.currentFlags[flag.key] { // Define Exposed flag and exposed visitor - var exposedFlag, exposedVisitor: String? + // var exposedFlag, exposedVisitor: String? + + var exposedFlag: FSExposedFlag? + var exposedVisitor: FSVisitorExposed? + if visitor.configManager.flagshipConfig.onVisitorExposed != nil { // Create flag exposed object - exposedFlag = FSExposedFlag(key: flag.key, defaultValue: flag.defaultValue, metadata: flag.metadata(), value: flag.value(defaultValue: flag.defaultValue, visitorExposed: false)).toJson() + exposedFlag = FSExposedFlag(key: flag.key, defaultValue: flag.defaultValue, metadata: flag.metadata(), value: flag.value(defaultValue: flag.defaultValue, visitorExposed: false)) // Create visitor expose object - exposedVisitor = FSVisitorExposed(id: visitor.visitorId, anonymousId: visitor.anonymousId, context: visitor.getContext()).toJson() + exposedVisitor = FSVisitorExposed(id: visitor.visitorId, anonymousId: visitor.anonymousId, context: visitor.getContext()) } - let activateToSend = Activate(visitor.visitorId, visitor.anonymousId, modification: aModification, exposedFlag, exposedVisitor) + let activateToSend = Activate(visitor.visitorId, visitor.anonymousId, modification: aModification, exposedFlag?.toJson(), exposedVisitor?.toJson()) /// If dediplicate if visitor.isDeduplicatedFlag(campId: flag.metadata().campaignId, varGrpId: flag.metadata().variationGroupId) { // Skip the activate print("Skip the activation ..... the variation is already activated") - /// Is callback is defined ===> Trigger it - if let aOnVisitorExposed = visitor.configManager.flagshipConfig.onVisitorExposed { - aOnVisitorExposed(FSVisitorExposed(id: visitor.visitorId, anonymousId: visitor.anonymousId, context: visitor.getContext()), FSExposedFlag(key: flag.key, defaultValue: flag.defaultValue, metadata: flag.metadata(), value: flag.value(defaultValue: flag.defaultValue, visitorExposed: false), alreadyActivatedCampaign: true)) + if let aExposedVisitor = exposedVisitor { + if let aExposedFlag = exposedFlag { + // Update exposed flag + aExposedFlag.alreadyActivatedCampaign = true + /// Is callback is defined ===> Trigger it + if let aOnVisitorExposed = visitor.configManager.flagshipConfig.onVisitorExposed { + aOnVisitorExposed(aExposedVisitor, aExposedFlag) + } + } } - + } else { visitor.configManager.trackingManager?.sendActivate(activateToSend, onCompletion: { error, exposedInfosArray in From f1c9d6cff612d720dd9ee80cb29f39da3f755bc5 Mon Sep 17 00:00:00 2001 From: Adel Date: Tue, 1 Jul 2025 12:55:40 +0200 Subject: [PATCH 3/8] clean code --- FlagShip/Source/Core/FSVisitor+Flags.swift | 51 +++++++------------ .../Core/FSVisitor+Reconcilliation.swift | 4 +- FlagShip/Source/Core/FSVisitor.swift | 24 ++++----- .../Source/Strategy/FSDefaultStrategy.swift | 4 +- 4 files changed, 34 insertions(+), 49 deletions(-) diff --git a/FlagShip/Source/Core/FSVisitor+Flags.swift b/FlagShip/Source/Core/FSVisitor+Flags.swift index 981d6f3f..77c3fc46 100644 --- a/FlagShip/Source/Core/FSVisitor+Flags.swift +++ b/FlagShip/Source/Core/FSVisitor+Flags.swift @@ -16,7 +16,7 @@ public extension FSVisitor { /// - Returns: FSFLag object func getFlag(key: String) -> FSFlag { /// Init the session - self.sessionStartDate = Date() + // self.sessionDuration = Date() // We dispaly a warning if the flag's status is not fetched if self.fetchStatus != .FETCHED { FlagshipLogManager.Log(level: .ALL, tag: .FLAG, messageToDisplay: FSLogMessage.MESSAGE(self.requiredFetchReason.warningMessage(key, self.visitorId))) @@ -28,7 +28,7 @@ public extension FSVisitor { /// - Returns: an instance of FSFlagCollection with flags func getFlags() -> FSFlagCollection { /// Init the session - self.sessionStartDate = Date() + // self.sessionDuration = Date() var ret: [String: FSFlag] = [:] self.currentFlags.forEach { (key: String, _: FSModification) in @@ -38,41 +38,26 @@ public extension FSVisitor { } internal func isDeduplicatedFlag(campId: String, varGrpId: String) -> Bool { - var ret = true - let duration = Date().timeIntervalSince(self.sessionStartDate) + let elapsed = Date().timeIntervalSince(self.sessionDuration) - print("the timeinterval duration is \(round(duration))") + print("⏱️ Temps écoulé : \(Int(elapsed)) s") - if round(duration) > FS_SESSION_VISITOR { - print("La session a duré plus de 30 minutes ----------") - print("vider les saves et envoyer les activates quant meeme") + // Reset the timestamp at the end of function + defer { sessionDuration = Date() } - // Clear all saved ids for flags - self.activatedVariations.removeAll() - - /// Register et ordonner l envoie - self.activatedVariations.merge([campId: varGrpId]) { _, new in new } - - ret = false - } else { - print("la session a moins que 30 minutes ===> alors on check avant d envoyer activate") - - if self.activatedVariations.keys.contains(campId) { - if varGrpId == self.activatedVariations[campId] { - /// The activation is already done - print("========== Already activated ============") - ret = true - } - } else { - /// Register et ordonner l envoie - self.activatedVariations.merge([campId: varGrpId]) { _, new in new } - print("========== GO for activation ============") - ret = false - } + // Session expired ==> reset the dico and return false + guard elapsed <= FS_SESSION_VISITOR else { + activatedVariations = [campId: varGrpId] + return false } - /// reset the timestamps - self.sessionStartDate = Date() - return ret + // Session still valide + if activatedVariations[campId] == varGrpId { + // The ids already exist ==> is deduplicated flag + return true + } + // We have a new flag ==> save it and return false + activatedVariations[campId] = varGrpId + return false } } diff --git a/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift b/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift index e56f2c04..9e1a54a4 100644 --- a/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift +++ b/FlagShip/Source/Core/FSVisitor+Reconcilliation.swift @@ -16,7 +16,7 @@ public extension FSVisitor { /// - Requires: Make sure that the experience continuity option is enabled on the flagship platform before using this method @objc func authenticate(visitorId: String) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() if configManager.flagshipConfig.mode != .DECISION_API { FlagshipLogManager.Log(level: .ALL, tag: .AUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_AUTHENTICATE) return @@ -30,7 +30,7 @@ public extension FSVisitor { /// Use authenticate methode to go from Logged in session to logged out session @objc func unauthenticate() { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() if configManager.flagshipConfig.mode != .DECISION_API { FlagshipLogManager.Log(level: .ALL, tag: .UNAUTHENTICATE, messageToDisplay: FSLogMessage.IGNORE_AUTHENTICATE) return diff --git a/FlagShip/Source/Core/FSVisitor.swift b/FlagShip/Source/Core/FSVisitor.swift index c108bbec..d731f8b3 100644 --- a/FlagShip/Source/Core/FSVisitor.swift +++ b/FlagShip/Source/Core/FSVisitor.swift @@ -88,8 +88,8 @@ import UIKit // Score value public internal(set) var emotionScoreAI: String? = nil - // sesstion timestamps - var sessionStartDate: Date + // Session duration in MS + var sessionDuration: Date // List of activated variations var activatedVariations: [String: String] = [:] /// campId:varGrpId @@ -157,12 +157,12 @@ import UIKit self._onFlagStatusFetched = pOnFlagStatusFetched // init sessionStartTimestamp - self.sessionStartDate = Date() + self.sessionDuration = Date() } @objc public func fetchFlags(onFetchCompleted: @escaping () -> Void) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() self.prepareEmotionAI(onCompleted: { score, _ in // Set the score self.emotionScoreAI = score @@ -225,7 +225,7 @@ import UIKit public func collectEmotionsAIEvents(window: UIWindow?, screenName: String? = nil, usingSwizzling: Bool = false) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() if Flagship.sharedInstance.eaiCollectEnabled == true { self.strategy?.getStrategy().collectEmotionsAIEvents(window: window, screenName: screenName, usingSwizzling: usingSwizzling) } else { @@ -245,7 +245,7 @@ import UIKit // - Parameter newContext: user's context @objc public func updateContext(_ context: [String: Any]) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() self._updateContext(context) } @@ -255,7 +255,7 @@ import UIKit // - newValue: value for teh given key public func updateContext(_ key: String, _ newValue: Any) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() self._updateContext([key: newValue]) } @@ -265,7 +265,7 @@ import UIKit // - newValue: the value for the given key public func updateContext(_ flagshipContext: FlagshipContext, _ value: Any) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() /// Check the validity value if !flagshipContext.chekcValidity(value) { FlagshipLogManager.Log(level: .ALL, tag: .UPDATE_CONTEXT, messageToDisplay: FSLogMessage.UPDATE_PRE_CONTEXT_FAILED(flagshipContext.rawValue)) @@ -286,14 +286,14 @@ import UIKit // - Returns: Dictionary that represent a user context @objc public func getContext() -> [String: Any] { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() return self.context.getCurrentContext() } // Clear the current context @objc public func clearContext() { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() self.context.clearContext() } @@ -301,7 +301,7 @@ import UIKit // - Parameter T: Hit object public func sendHit(_ event: T) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() self.strategy?.getStrategy().sendHit(event) } @@ -312,7 +312,7 @@ import UIKit // - Parameter newValue: if true, then flush all stored visitor data @objc public func setConsent(hasConsented: Bool) { /// Init the session - self.sessionStartDate = Date() + self.sessionDuration = Date() self.hasConsented = hasConsented self.strategy?.getStrategy().setConsent(newValue: hasConsented) diff --git a/FlagShip/Source/Strategy/FSDefaultStrategy.swift b/FlagShip/Source/Strategy/FSDefaultStrategy.swift index 0aa369d7..27ff5bc9 100644 --- a/FlagShip/Source/Strategy/FSDefaultStrategy.swift +++ b/FlagShip/Source/Strategy/FSDefaultStrategy.swift @@ -56,7 +56,7 @@ class FSDefaultStrategy: FSDelegateStrategy { // Create flag exposed object exposedFlag = FSExposedFlag(key: flag.key, defaultValue: flag.defaultValue, metadata: flag.metadata(), value: flag.value(defaultValue: flag.defaultValue, visitorExposed: false)) // Create visitor expose object - exposedVisitor = FSVisitorExposed(id: visitor.visitorId, anonymousId: visitor.anonymousId, context: visitor.getContext()) + exposedVisitor = FSVisitorExposed(id: visitor.visitorId, anonymousId: visitor.anonymousId, context: visitor.context.currentContext) } let activateToSend = Activate(visitor.visitorId, visitor.anonymousId, modification: aModification, exposedFlag?.toJson(), exposedVisitor?.toJson()) @@ -64,7 +64,7 @@ class FSDefaultStrategy: FSDelegateStrategy { /// If dediplicate if visitor.isDeduplicatedFlag(campId: flag.metadata().campaignId, varGrpId: flag.metadata().variationGroupId) { // Skip the activate - print("Skip the activation ..... the variation is already activated") + print("Skip the activation ..... The flag's variation is already activated") if let aExposedVisitor = exposedVisitor { if let aExposedFlag = exposedFlag { // Update exposed flag From f859784ecabbb907c1cddbf0f3dabcd665615f07 Mon Sep 17 00:00:00 2001 From: Adel Date: Wed, 2 Jul 2025 10:38:48 +0200 Subject: [PATCH 4/8] clean code --- FlagShip/FlagShipTests/FSOnExposureTest.swift | 7 +- FlagShip/Source/Core/FSExposedFlag.swift | 4 +- FlagShip/Source/Core/FSVisitor+Flags.swift | 2 +- FlagShip/Source/Core/FSVisitorExposed.swift | 4 +- .../Source/Strategy/FSDefaultStrategy.swift | 111 ++++++++++-------- iOS Example/QApp/FSConfigViewController.swift | 12 +- 6 files changed, 76 insertions(+), 64 deletions(-) diff --git a/FlagShip/FlagShipTests/FSOnExposureTest.swift b/FlagShip/FlagShipTests/FSOnExposureTest.swift index e8b3384b..8e914163 100644 --- a/FlagShip/FlagShipTests/FSOnExposureTest.swift +++ b/FlagShip/FlagShipTests/FSOnExposureTest.swift @@ -85,7 +85,7 @@ final class FSOnExposureTest: XCTestCase { XCTAssertTrue(flagTest.metadata.campaignId == "campId") XCTAssertTrue(flagTest.toDictionary()["alreadyActivatedCampaign"] as? Bool == true) - XCTAssertTrue(flagTest.toJson()?.count ?? 0 > 0) + XCTAssertTrue(flagTest.toJson()?.length ?? 0 > 0) } // Test Visitor Object @@ -94,18 +94,16 @@ final class FSOnExposureTest: XCTestCase { XCTAssertTrue(visitorObject.toDictionary()["id"] as? String == "testId") XCTAssertTrue(visitorObject.toDictionary()["anonymousId"] as? String == "ano1") XCTAssertTrue((visitorObject.toDictionary()["context"] as? [String: Any])?["key1"] as? String == "val1") - XCTAssertTrue(visitorObject.toJson()?.count ?? 0 > 0) + XCTAssertTrue(visitorObject.toJson()?.length ?? 0 > 0) } func testDeduplication() { let expectationSync = XCTestExpectation(description: "Service-deduplication") - testVisitor?.fetchFlags(onFetchCompleted: { if let flag = self.testVisitor?.getFlag(key: "btnTitle") { XCTAssertTrue(flag.value(defaultValue: "dfl") == "Alpha_demoApp") // Activate again XCTAssertTrue(flag.value(defaultValue: "dfl") == "Alpha_demoApp") - // Check deduplication XCTAssertTrue(self.testVisitor?.isDeduplicatedFlag(campId: "bvcdqksmicqghldq9agg", varGrpId: "bvcdqksmicqghldq9ahg") ?? false) // Send hit to reset timer @@ -113,7 +111,6 @@ final class FSOnExposureTest: XCTestCase { XCTAssertTrue(self.testVisitor?.isDeduplicatedFlag(campId: "bvcdqksmicqghldq9agg", varGrpId: "bvcdqksmicqghldq9ahg") ?? false) } expectationSync.fulfill() - }) wait(for: [expectationSync], timeout: 5.0) } diff --git a/FlagShip/Source/Core/FSExposedFlag.swift b/FlagShip/Source/Core/FSExposedFlag.swift index 1a382dc4..9716a349 100644 --- a/FlagShip/Source/Core/FSExposedFlag.swift +++ b/FlagShip/Source/Core/FSExposedFlag.swift @@ -80,7 +80,7 @@ protocol IFlag { /// String that represent a json for the Exposed Flag /// - Return: String? - @objc public func toJson() -> String? { + @objc public func toJson() -> NSString? { var result: [String: Any] = [ "key": key, "metadata": metadata.toJson() @@ -101,6 +101,6 @@ protocol IFlag { guard let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) else { return nil } - return jsonData.prettyPrintedJSONString as String? + return jsonData.prettyPrintedJSONString } } diff --git a/FlagShip/Source/Core/FSVisitor+Flags.swift b/FlagShip/Source/Core/FSVisitor+Flags.swift index 77c3fc46..cb3bc86f 100644 --- a/FlagShip/Source/Core/FSVisitor+Flags.swift +++ b/FlagShip/Source/Core/FSVisitor+Flags.swift @@ -40,7 +40,7 @@ public extension FSVisitor { internal func isDeduplicatedFlag(campId: String, varGrpId: String) -> Bool { let elapsed = Date().timeIntervalSince(self.sessionDuration) - print("⏱️ Temps écoulé : \(Int(elapsed)) s") + // print("⏱️ Time session since last activity is : \(Int(elapsed)) s") // Reset the timestamp at the end of function defer { sessionDuration = Date() } diff --git a/FlagShip/Source/Core/FSVisitorExposed.swift b/FlagShip/Source/Core/FSVisitorExposed.swift index 14b065b2..a3b98d13 100644 --- a/FlagShip/Source/Core/FSVisitorExposed.swift +++ b/FlagShip/Source/Core/FSVisitorExposed.swift @@ -47,7 +47,7 @@ import Foundation /// String that represent a json for the Visitor Exposed /// - Return: NSString ? - @objc public func toJson() -> String? { + @objc public func toJson() -> NSString? { var result: [String: Any] = [ "id": id, "context": context @@ -60,7 +60,7 @@ import Foundation guard let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) else { return nil } - return jsonData.jsonString + return jsonData.prettyPrintedJSONString } } diff --git a/FlagShip/Source/Strategy/FSDefaultStrategy.swift b/FlagShip/Source/Strategy/FSDefaultStrategy.swift index 27ff5bc9..806afdff 100644 --- a/FlagShip/Source/Strategy/FSDefaultStrategy.swift +++ b/FlagShip/Source/Strategy/FSDefaultStrategy.swift @@ -43,59 +43,74 @@ class FSDefaultStrategy: FSDelegateStrategy { self.visitor = pVisitor } - /// Activate Flag func activateFlag(_ flag: FSFlag) { - if let aModification = visitor.currentFlags[flag.key] { - // Define Exposed flag and exposed visitor - // var exposedFlag, exposedVisitor: String? - - var exposedFlag: FSExposedFlag? - var exposedVisitor: FSVisitorExposed? - - if visitor.configManager.flagshipConfig.onVisitorExposed != nil { - // Create flag exposed object - exposedFlag = FSExposedFlag(key: flag.key, defaultValue: flag.defaultValue, metadata: flag.metadata(), value: flag.value(defaultValue: flag.defaultValue, visitorExposed: false)) - // Create visitor expose object - exposedVisitor = FSVisitorExposed(id: visitor.visitorId, anonymousId: visitor.anonymousId, context: visitor.context.currentContext) + // Exit if we don’t have a modification present in current flag + guard let modification = visitor.currentFlags[flag.key] else { return } + + // Get the informations + let flagshipConfig = visitor.configManager.flagshipConfig + let callback = flagshipConfig.onVisitorExposed + let metadata = flag.metadata() + let value = flag.value(defaultValue: flag.defaultValue, visitorExposed: false) + + // Prepare objetcs when callback exist + var exposedFlag: FSExposedFlag? + var exposedVisitor: FSVisitorExposed? + if callback != nil { + exposedFlag = FSExposedFlag( + key: flag.key, + defaultValue: flag.defaultValue, + metadata: metadata, + value: value + ) + exposedVisitor = FSVisitorExposed( + id: visitor.visitorId, + anonymousId: visitor.anonymousId, + context: visitor.context.currentContext + ) + } + + // Build the activation hit + let activateToSend = Activate( + visitor.visitorId, + visitor.anonymousId, + modification: modification, String(exposedFlag?.toJson() ?? "null"), + String(exposedVisitor?.toJson() ?? "null"), + ) + + // Handle deduplication before sending hit + let isDuplicate = visitor.isDeduplicatedFlag( + campId: metadata.campaignId, + varGrpId: metadata.variationGroupId + ) + if isDuplicate { + FlagshipLogManager.Log(level: .DEBUG, tag: .ACTIVATE, messageToDisplay: FSLogMessage.MESSAGE("Skip sending activation… variation already activated in this current session.")) + // if we have an exposedFlag, mark it and fire callback once + if let ef = exposedFlag, let ev = exposedVisitor { + ef.alreadyActivatedCampaign = true + callback?(ev, ef) } - - let activateToSend = Activate(visitor.visitorId, visitor.anonymousId, modification: aModification, exposedFlag?.toJson(), exposedVisitor?.toJson()) - - /// If dediplicate - if visitor.isDeduplicatedFlag(campId: flag.metadata().campaignId, varGrpId: flag.metadata().variationGroupId) { - // Skip the activate - print("Skip the activation ..... The flag's variation is already activated") - if let aExposedVisitor = exposedVisitor { - if let aExposedFlag = exposedFlag { - // Update exposed flag - aExposedFlag.alreadyActivatedCampaign = true - /// Is callback is defined ===> Trigger it - if let aOnVisitorExposed = visitor.configManager.flagshipConfig.onVisitorExposed { - aOnVisitorExposed(aExposedVisitor, aExposedFlag) - } - } - } - - } else { - visitor.configManager.trackingManager?.sendActivate(activateToSend, onCompletion: { error, exposedInfosArray in - - if error == nil { - /// Is callback is defined ===> Trigger it - if let aOnVisitorExposed = self.visitor.configManager.flagshipConfig.onVisitorExposed { - exposedInfosArray?.forEach { item in - aOnVisitorExposed(item.visitorExposed, item.exposedFlag) - } - } - } else { - // The flag error - } - }) - // Troubleshooitng activate - FSDataUsageTracking.sharedInstance.processTSHits(label: CriticalPoints.VISITOR_SEND_ACTIVATE.rawValue, visitor: visitor, hit: activateToSend) + return + } + + // Send activation + visitor.configManager.trackingManager?.sendActivate( + activateToSend + ) { error, exposedInfosArray in + guard error == nil, let infos = exposedInfosArray else { return } + infos.forEach { info in + callback?(info.visitorExposed, info.exposedFlag) } } + + // Troubleshooting / TS hit + FSDataUsageTracking.sharedInstance.processTSHits( + label: CriticalPoints.VISITOR_SEND_ACTIVATE.rawValue, + visitor: visitor, + hit: activateToSend + ) } - + func synchronize(onSyncCompleted: @escaping (FSFlagStatus, FetchFlagsRequiredStatusReason) -> Void) { let startFetchingDate = Date() // To comunicate for TR diff --git a/iOS Example/QApp/FSConfigViewController.swift b/iOS Example/QApp/FSConfigViewController.swift index cfc857e2..853be57f 100644 --- a/iOS Example/QApp/FSConfigViewController.swift +++ b/iOS Example/QApp/FSConfigViewController.swift @@ -103,11 +103,11 @@ class FSConfigViewController: UIViewController, UITextFieldDelegate, FSJsonEdito } } } - }.withLogLevel(.ALL).withOnVisitorExposed { visitorExposed, fromFlag in + }.withLogLevel(.ALL).withOnVisitorExposed { v, fromFlag in + + print("------- On visitor exposed callback ----------") + print(v.toJson()) - //print("------- On visitor exposed callback ----------") - // print(visitorExposed.toJson()) - print(fromFlag.toJson()) print("------- On visitor exposed callback ----------") } @@ -167,9 +167,9 @@ class FSConfigViewController: UIViewController, UITextFieldDelegate, FSJsonEdito } func createVisitor() -> FSVisitor { - let userIdToSet: String = visitorIdTextField?.text ?? "" + let userIdToSet: String = visitorIdTextField?.text ?? "" - //let userIdToSet = "iosUser_\(UUID().uuidString)" + // let userIdToSet = "iosUser_\(UUID().uuidString)" return Flagship.sharedInstance.newVisitor(visitorId: userIdToSet, hasConsented: allowTrackingSwitch?.isOn ?? true).withContext(context: ["segment": "coffee", "isQA": true, "testing_tracking_manager": true, "isPreRelease": true, "test": 12]).isAuthenticated(authenticateSwitch?.isOn ?? false).withOnFlagStatusChanged { newStatus in From 64618c850a32c7283c526ae69e67defd7899de19 Mon Sep 17 00:00:00 2001 From: Adel Date: Wed, 2 Jul 2025 11:52:01 +0200 Subject: [PATCH 5/8] fix review --- FlagShip/FlagShipTests/FSOnExposureTest.swift | 27 +++++++++++++++++++ FlagShip/Source/Core/FSExposedFlag.swift | 13 +++++---- FlagShip/Source/Core/FSVisitor+Flags.swift | 6 ++--- FlagShip/Source/Core/FSVisitor.swift | 2 +- FlagShip/Source/Models/Tracking/FSBatch.swift | 2 +- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/FlagShip/FlagShipTests/FSOnExposureTest.swift b/FlagShip/FlagShipTests/FSOnExposureTest.swift index 8e914163..d3c6c896 100644 --- a/FlagShip/FlagShipTests/FSOnExposureTest.swift +++ b/FlagShip/FlagShipTests/FSOnExposureTest.swift @@ -86,6 +86,21 @@ final class FSOnExposureTest: XCTestCase { XCTAssertTrue(flagTest.toDictionary()["alreadyActivatedCampaign"] as? Bool == true) XCTAssertTrue(flagTest.toJson()?.length ?? 0 > 0) + + var dicoFlag = flagTest.toDictionary() + + // create FSExposedFlag obj + let obj = FSExposedFlag(exposedInfo: dicoFlag) + + // Modify the dico flag + dicoFlag.removeValue(forKey: "alreadyActivatedCampaign") + let objBis = FSExposedFlag(exposedInfo: dicoFlag) + XCTAssertFalse(objBis.alreadyActivatedCampaign ?? true) // true to rise error + + // Modify the entire object + dicoFlag.removeAll() + let objTer = FSExposedFlag(exposedInfo: dicoFlag) + XCTAssertTrue(objTer.metadata.campaignId.count == 0) } // Test Visitor Object @@ -95,6 +110,18 @@ final class FSOnExposureTest: XCTestCase { XCTAssertTrue(visitorObject.toDictionary()["anonymousId"] as? String == "ano1") XCTAssertTrue((visitorObject.toDictionary()["context"] as? [String: Any])?["key1"] as? String == "val1") XCTAssertTrue(visitorObject.toJson()?.length ?? 0 > 0) + + var dico = visitorObject.toDictionary() + + // Modify dico + dico.removeValue(forKey: "context") + let obj = FSVisitorExposed(dico: dico) + XCTAssertTrue(obj.context.isEmpty) + + // Modify dico + dico.removeAll() + let objBis = FSVisitorExposed(dico: dico) + XCTAssertNil(objBis.anonymousId) } func testDeduplication() { diff --git a/FlagShip/Source/Core/FSExposedFlag.swift b/FlagShip/Source/Core/FSExposedFlag.swift index 9716a349..8610d60c 100644 --- a/FlagShip/Source/Core/FSExposedFlag.swift +++ b/FlagShip/Source/Core/FSExposedFlag.swift @@ -19,7 +19,6 @@ protocol IFlag { var metadata: FSFlagMetadata { get set } // Already activated campaign - var alreadyActivatedCampaign: Bool? { get } } @@ -47,12 +46,12 @@ protocol IFlag { self.alreadyActivatedCampaign = alreadyActivatedCampaign } - init(expoedInfo: [String: Any]) { - self.key = expoedInfo["key"] as? String ?? "" - self.value = expoedInfo["value"] - self.defaultValue = expoedInfo["defaultValue"] - self.metadata = FSFlagMetadata(metadataDico: expoedInfo["metadata"] as? [String: Any] ?? [:]) - self.alreadyActivatedCampaign = expoedInfo["alreadyActivatedCampaign"] as? Bool ?? false // TODO: Review & test later + init(exposedInfo: [String: Any]) { + self.key = exposedInfo["key"] as? String ?? "" + self.value = exposedInfo["value"] + self.defaultValue = exposedInfo["defaultValue"] + self.metadata = FSFlagMetadata(metadataDico: exposedInfo["metadata"] as? [String: Any] ?? [:]) + self.alreadyActivatedCampaign = exposedInfo["alreadyActivatedCampaign"] as? Bool ?? false // TODO: Review & test later } /// Dictionary that represent the Exposed Flag diff --git a/FlagShip/Source/Core/FSVisitor+Flags.swift b/FlagShip/Source/Core/FSVisitor+Flags.swift index cb3bc86f..b78495ed 100644 --- a/FlagShip/Source/Core/FSVisitor+Flags.swift +++ b/FlagShip/Source/Core/FSVisitor+Flags.swift @@ -8,7 +8,7 @@ import Foundation -let FS_SESSION_VISITOR: TimeInterval = 30.0 // 30 * 60 /// 30 mn +let FS_SESSION_VISITOR: TimeInterval = 30 * 60 /// 30 mn public extension FSVisitor { /// Get FSFlag object @@ -40,10 +40,10 @@ public extension FSVisitor { internal func isDeduplicatedFlag(campId: String, varGrpId: String) -> Bool { let elapsed = Date().timeIntervalSince(self.sessionDuration) - // print("⏱️ Time session since last activity is : \(Int(elapsed)) s") + // print("⏱️ Time session since last activity is : \(Int(elapsed)) s") // Reset the timestamp at the end of function - defer { sessionDuration = Date() } + defer { sessionDuration = Date() } // Session expired ==> reset the dico and return false guard elapsed <= FS_SESSION_VISITOR else { diff --git a/FlagShip/Source/Core/FSVisitor.swift b/FlagShip/Source/Core/FSVisitor.swift index d731f8b3..26b08f3e 100644 --- a/FlagShip/Source/Core/FSVisitor.swift +++ b/FlagShip/Source/Core/FSVisitor.swift @@ -88,7 +88,7 @@ import UIKit // Score value public internal(set) var emotionScoreAI: String? = nil - // Session duration in MS + // Session duration var sessionDuration: Date // List of activated variations diff --git a/FlagShip/Source/Models/Tracking/FSBatch.swift b/FlagShip/Source/Models/Tracking/FSBatch.swift index c7069deb..f3c53a07 100644 --- a/FlagShip/Source/Models/Tracking/FSBatch.swift +++ b/FlagShip/Source/Models/Tracking/FSBatch.swift @@ -176,7 +176,7 @@ class Activate: FSTrackingProtocol, Codable { if let dataExposedFalg = self.exposure_flag?.data(using: .utf8), let dataExposedVisitor = self.exposure_visitor?.data(using: .utf8) { do { if let dicoExposedFalg = (try? JSONSerialization.jsonObject(with: dataExposedFalg, options: [])) as? [String: Any] { - let expoFlag = FSExposedFlag(expoedInfo: dicoExposedFalg) + let expoFlag = FSExposedFlag(exposedInfo: dicoExposedFalg) if let dicoExposedVisitor = (try? JSONSerialization.jsonObject(with: dataExposedVisitor, options: [])) as? [String: Any] { let expoVisitor = FSVisitorExposed(dico: dicoExposedVisitor) From 21f7231ed8dea1761294f9282a71b508102479de Mon Sep 17 00:00:00 2001 From: Adel Date: Thu, 3 Jul 2025 16:58:06 +0200 Subject: [PATCH 6/8] update version --- FlagShip.podspec | 2 +- FlagShip/Flagship.xcodeproj/project.pbxproj | 4 ++-- FlagShip/Source/Core/FSExposedFlag.swift | 6 +++--- FlagShip/Source/Core/FSVisitorExposed.swift | 6 +++--- FlagShip/Source/Tools/FlagShipVersion.swift | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/FlagShip.podspec b/FlagShip.podspec index daac6e3c..465aed86 100644 --- a/FlagShip.podspec +++ b/FlagShip.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = "FlagShip" - s.version = "5.0.0-beta.5" + s.version = "5.1.0-beta.5" s.summary = "Flagship SDK" # This description is used to generate tags and improve search results. diff --git a/FlagShip/Flagship.xcodeproj/project.pbxproj b/FlagShip/Flagship.xcodeproj/project.pbxproj index c2a3b733..6c8db052 100644 --- a/FlagShip/Flagship.xcodeproj/project.pbxproj +++ b/FlagShip/Flagship.xcodeproj/project.pbxproj @@ -1900,7 +1900,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "5.0.0-beta.5"; + MARKETING_VERSION = "5.1.0-beta.5"; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ABTasty.FlagShip; @@ -1934,7 +1934,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "5.0.0-beta.5"; + MARKETING_VERSION = "5.1.0-beta.5"; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ABTasty.FlagShip; diff --git a/FlagShip/Source/Core/FSExposedFlag.swift b/FlagShip/Source/Core/FSExposedFlag.swift index 8610d60c..dc958185 100644 --- a/FlagShip/Source/Core/FSExposedFlag.swift +++ b/FlagShip/Source/Core/FSExposedFlag.swift @@ -79,7 +79,7 @@ protocol IFlag { /// String that represent a json for the Exposed Flag /// - Return: String? - @objc public func toJson() -> NSString? { + @objc public func toJson() -> NSString { var result: [String: Any] = [ "key": key, "metadata": metadata.toJson() @@ -98,8 +98,8 @@ protocol IFlag { } guard let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) else { - return nil + return "" } - return jsonData.prettyPrintedJSONString + return jsonData.prettyPrintedJSONString ?? "" } } diff --git a/FlagShip/Source/Core/FSVisitorExposed.swift b/FlagShip/Source/Core/FSVisitorExposed.swift index a3b98d13..9737afee 100644 --- a/FlagShip/Source/Core/FSVisitorExposed.swift +++ b/FlagShip/Source/Core/FSVisitorExposed.swift @@ -47,7 +47,7 @@ import Foundation /// String that represent a json for the Visitor Exposed /// - Return: NSString ? - @objc public func toJson() -> NSString? { + @objc public func toJson() -> NSString { var result: [String: Any] = [ "id": id, "context": context @@ -58,9 +58,9 @@ import Foundation } guard let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) else { - return nil + return "" } - return jsonData.prettyPrintedJSONString + return jsonData.prettyPrintedJSONString ?? "" } } diff --git a/FlagShip/Source/Tools/FlagShipVersion.swift b/FlagShip/Source/Tools/FlagShipVersion.swift index 23acfc2d..e0b5158b 100644 --- a/FlagShip/Source/Tools/FlagShipVersion.swift +++ b/FlagShip/Source/Tools/FlagShipVersion.swift @@ -8,4 +8,4 @@ import Foundation /// This file is automatically updated 2.0.0 -public let FlagShipVersion = "5.0.0-beta.5" +public let FlagShipVersion = "5.1.0-beta.5" From 1fd84209defa7d760b7f09026faf229dedde3354 Mon Sep 17 00:00:00 2001 From: Adel Date: Thu, 3 Jul 2025 17:45:05 +0200 Subject: [PATCH 7/8] secure code --- FlagShip/Source/Core/FSExposedFlag.swift | 4 ++-- FlagShip/Source/Core/FSVisitor.swift | 15 ++++++++++++++- FlagShip/Source/Core/FSVisitorExposed.swift | 4 ++-- FlagShip/Source/Strategy/FSDefaultStrategy.swift | 6 +++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/FlagShip/Source/Core/FSExposedFlag.swift b/FlagShip/Source/Core/FSExposedFlag.swift index dc958185..8352b6e5 100644 --- a/FlagShip/Source/Core/FSExposedFlag.swift +++ b/FlagShip/Source/Core/FSExposedFlag.swift @@ -79,7 +79,7 @@ protocol IFlag { /// String that represent a json for the Exposed Flag /// - Return: String? - @objc public func toJson() -> NSString { + @objc public func toJson() -> String? { var result: [String: Any] = [ "key": key, "metadata": metadata.toJson() @@ -100,6 +100,6 @@ protocol IFlag { guard let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) else { return "" } - return jsonData.prettyPrintedJSONString ?? "" + return jsonData.jsonString } } diff --git a/FlagShip/Source/Core/FSVisitor.swift b/FlagShip/Source/Core/FSVisitor.swift index 26b08f3e..34895797 100644 --- a/FlagShip/Source/Core/FSVisitor.swift +++ b/FlagShip/Source/Core/FSVisitor.swift @@ -61,9 +61,22 @@ import UIKit self.configManager.updateAid(newValue) } } + + var currentFlags: [String: FSModification] { + get { + return self.fsQueue.sync { + self._currentFlags + } + } + set { + self.fsQueue.async(flags: .barrier) { + self._currentFlags = newValue + } + } + } /// Modifications - var currentFlags: [String: FSModification] = [:] + private var _currentFlags: [String: FSModification] = [:] /// Context var context: FSContext /// Strategy diff --git a/FlagShip/Source/Core/FSVisitorExposed.swift b/FlagShip/Source/Core/FSVisitorExposed.swift index 9737afee..723be93d 100644 --- a/FlagShip/Source/Core/FSVisitorExposed.swift +++ b/FlagShip/Source/Core/FSVisitorExposed.swift @@ -47,7 +47,7 @@ import Foundation /// String that represent a json for the Visitor Exposed /// - Return: NSString ? - @objc public func toJson() -> NSString { + @objc public func toJson() -> String? { var result: [String: Any] = [ "id": id, "context": context @@ -60,7 +60,7 @@ import Foundation guard let jsonData = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) else { return "" } - return jsonData.prettyPrintedJSONString ?? "" + return jsonData.jsonString } } diff --git a/FlagShip/Source/Strategy/FSDefaultStrategy.swift b/FlagShip/Source/Strategy/FSDefaultStrategy.swift index 806afdff..203c0497 100644 --- a/FlagShip/Source/Strategy/FSDefaultStrategy.swift +++ b/FlagShip/Source/Strategy/FSDefaultStrategy.swift @@ -74,10 +74,10 @@ class FSDefaultStrategy: FSDelegateStrategy { let activateToSend = Activate( visitor.visitorId, visitor.anonymousId, - modification: modification, String(exposedFlag?.toJson() ?? "null"), - String(exposedVisitor?.toJson() ?? "null"), + modification: modification, exposedFlag?.toJson(), + exposedVisitor?.toJson() ) - + // Handle deduplication before sending hit let isDuplicate = visitor.isDeduplicatedFlag( campId: metadata.campaignId, From bd46dec78e7a7d00f4c87e1dd222aac09b450cfb Mon Sep 17 00:00:00 2001 From: Adel Date: Thu, 16 Oct 2025 11:12:29 +0200 Subject: [PATCH 8/8] update verison --- FlagShip.podspec | 2 +- FlagShip/Flagship.xcodeproj/project.pbxproj | 4 ++-- FlagShip/Source/Tools/FlagShipVersion.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FlagShip.podspec b/FlagShip.podspec index 465aed86..6b9ae890 100644 --- a/FlagShip.podspec +++ b/FlagShip.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = "FlagShip" - s.version = "5.1.0-beta.5" + s.version = "5.2.0-beta.0" s.summary = "Flagship SDK" # This description is used to generate tags and improve search results. diff --git a/FlagShip/Flagship.xcodeproj/project.pbxproj b/FlagShip/Flagship.xcodeproj/project.pbxproj index 6c8db052..4182af7c 100644 --- a/FlagShip/Flagship.xcodeproj/project.pbxproj +++ b/FlagShip/Flagship.xcodeproj/project.pbxproj @@ -1900,7 +1900,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "5.1.0-beta.5"; + MARKETING_VERSION = "5.2.0-beta.0"; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ABTasty.FlagShip; @@ -1934,7 +1934,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "5.1.0-beta.5"; + MARKETING_VERSION = "5.2.0-beta.0"; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ABTasty.FlagShip; diff --git a/FlagShip/Source/Tools/FlagShipVersion.swift b/FlagShip/Source/Tools/FlagShipVersion.swift index e0b5158b..6c08ffe4 100644 --- a/FlagShip/Source/Tools/FlagShipVersion.swift +++ b/FlagShip/Source/Tools/FlagShipVersion.swift @@ -8,4 +8,4 @@ import Foundation /// This file is automatically updated 2.0.0 -public let FlagShipVersion = "5.1.0-beta.5" +public let FlagShipVersion = "5.2.0-beta.0"