Skip to content

Commit

Permalink
Merge pull request #480 from splitio/release_2.23.0
Browse files Browse the repository at this point in the history
Release 2.23.0
  • Loading branch information
javrudsky authored Nov 1, 2023
2 parents 3c70797 + de4a80f commit b3222fd
Show file tree
Hide file tree
Showing 120 changed files with 3,826 additions and 357 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/test_ios_streaming_1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build and Test iOS Streaming 1

on:
push:
branches:
- master
pull_request:
branches:
- master
- development

jobs:
build:
runs-on: [macos-latest]

steps:
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 13.2.1

- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0

- name: Test iOS Streaming integration
uses: sersoft-gmbh/xcodebuild-action@v1
with:
action: build test
build-settings: ONLY_ACTIVE_ARCH=NO TEST_AFTER_BUILD=YES
configuration: Debug
derived-data-path: "${{github.workspace}}/SplitApp"
destination: 'platform=iOS Simulator,OS=15.2,name=iPhone 12'
project: Split.xcodeproj
scheme: Split
sdk: 'iphonesimulator'
test-plan: 'SplitiOSStreaming_1'
use-xcpretty: true
10 changes: 10 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
2.23.0: (Nov 1, 2023)
- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation):
- Added new variations of the get treatment methods to support evaluating flags in given flag set/s.
- getTreatmentsByFlagSet and getTreatmentsByFlagSets
- getTreatmentWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets
- Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload.
- Updated the following SDK manager methods to expose flag sets on flag views.
- Added `defaultTreatment` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager.
- Removed APIs that were not in use and required a privacy manifest.

2.22.0: (Aug 14, 2023)
- Adding support to use the SDK in extensions. Thanks @Jeff-Meadows!

Expand Down
4 changes: 2 additions & 2 deletions Split.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'Split'
s.module_name = 'Split'
s.version = '2.22.0'
s.version = '2.23.0'
s.summary = 'iOS SDK for Split'
s.description = <<-DESC
This SDK is designed to work with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience.
Expand All @@ -11,7 +11,7 @@ This SDK is designed to work with Split, the platform for controlled rollouts, s
s.license = { :type => 'Apache 2.0', :file => 'LICENSE' }
s.author = { 'Patricio Echague' => '[email protected]', 'Sebastian Arrubia' => '[email protected]', 'Fernando Martin' => '[email protected]'}
s.source = { :git => 'https://github.com/splitio/ios-client.git', :tag => s.version.to_s }
s.platforms = { :ios => "9.0", :osx => "10.11", :watchos => "7.0", :tvos => "9.0" }
s.platforms = { :ios => "12.0", :osx => "10.13", :watchos => "7.0", :tvos => "12.0" }
s.frameworks = 'Foundation'
s.swift_versions = ['4.0', '4.2', '5.0', '5.1', '5.2', '5.3']
s.resources = "Split/Storage/split_cache.xcdatamodeld"
Expand Down
82 changes: 75 additions & 7 deletions Split.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions Split.xcodeproj/xcshareddata/xcschemes/Split.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
Expand Down Expand Up @@ -70,6 +70,9 @@
<TestPlanReference
reference = "container:SplitPushManagerUT.xctestplan">
</TestPlanReference>
<TestPlanReference
reference = "container:SplitiOSStreaming_1.xctestplan">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
Expand Down
27 changes: 24 additions & 3 deletions Split/Api/DefaultSplitClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ extension DefaultSplitClient {
if event != .sdkReadyFromCache,
eventsManager.eventAlreadyTriggered(event: event) {
Logger.w("A handler was added for \(event.toString()) on the SDK, " +
"which has already fired and won’t be emitted again. The callback won’t be executed.")
"which has already fired and won’t be emitted again. The callback won’t be executed.")
return
}
let task = SplitEventActionTask(action: action)
Expand Down Expand Up @@ -190,19 +190,40 @@ extension DefaultSplitClient {

private func isValidAttribute(_ value: Any) -> Bool {
return anyValueValidator.isPrimitiveValue(value: value) ||
anyValueValidator.isList(value: value)
anyValueValidator.isList(value: value)
}

private func logInvalidAttribute(name: String) {
Logger.i("Invalid attribute value for evaluation: \(name). " +
"Types allowed are String, Number, Boolean and List")
"Types allowed are String, Number, Boolean and List")
}

private func attributesStorage() -> AttributesStorage {
return storageContainer.attributesStorage
}
}

// MARK: By Sets evaluation
extension DefaultSplitClient {

public func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String] {
return treatmentManager.getTreatmentsByFlagSet(flagSet: flagSet, attributes: attributes)
}

public func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: String] {
return treatmentManager.getTreatmentsByFlagSets(flagSets: flagSets, attributes: attributes)
}

public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: SplitResult] {
return treatmentManager.getTreatmentsWithConfigByFlagSet(flagSet: flagSet, attributes: attributes)
}

public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String],
attributes: [String: Any]?) -> [String: SplitResult] {
return treatmentManager.getTreatmentsWithConfigByFlagSets(flagSets: flagSets, attributes: attributes)
}
}

// MARK: Flush / Destroy
extension DefaultSplitClient {

Expand Down
9 changes: 6 additions & 3 deletions Split/Api/DefaultSplitFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public class DefaultSplitFactory: NSObject, SplitFactory {
}

private var defaultManager: SplitManager?
private let filterBuilder = FilterBuilder()

public var client: SplitClient {
if let client = clientManager?.defaultClient {
Expand Down Expand Up @@ -85,7 +84,7 @@ public class DefaultSplitFactory: NSObject, SplitFactory {

userConsentManager = try components.buildUserConsentManager()

setupBgSync(config: params.config, apiKey: params.apiKey, userKey: params.key.matchingKey)
setupBgSync(config: params.config, apiKey: params.apiKey, userKey: params.key.matchingKey, storageContainer: storageContainer)

clientManager = DefaultClientManager(config: params.config,
key: params.key,
Expand Down Expand Up @@ -128,10 +127,14 @@ public class DefaultSplitFactory: NSObject, SplitFactory {
userConsentManager.set(newMode)
}

private func setupBgSync(config: SplitClientConfig, apiKey: String, userKey: String) {
private func setupBgSync(config: SplitClientConfig,
apiKey: String,
userKey: String,
storageContainer: SplitStorageContainer) {
#if os(iOS)
if config.synchronizeInBackground {
SplitBgSynchronizer.shared.register(apiKey: apiKey, userKey: userKey)
storageContainer.splitsStorage.update(bySetsFilter: config.bySetsFilter())
} else {
SplitBgSynchronizer.shared.unregister(apiKey: apiKey, userKey: userKey)
}
Expand Down
2 changes: 2 additions & 0 deletions Split/Api/DefaultSplitManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ import Foundation
splitView.name = split.name
splitView.changeNumber = split.changeNumber
splitView.trafficType = split.trafficTypeName
splitView.defaultTreatment = split.defaultTreatment
splitView.killed = split.killed
splitView.sets = Array(split.sets ?? [])
splitView.configs = split.configurations ?? [String: String]()

if let conditions = split.conditions {
Expand Down
16 changes: 16 additions & 0 deletions Split/Api/FailHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ class FailedClient: SplitClient {
func track(eventType: String, value: Double, properties: [String: Any]?) -> Bool {
return false
}

func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : String] {
return [:]
}

func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String : String] {
return [:]
}

func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : SplitResult] {
return [:]
}

func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String : SplitResult] {
return [:]
}
}

class FailedManager: SplitManager {
Expand Down
34 changes: 26 additions & 8 deletions Split/Api/FilterBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@

import Foundation

class MaxFilterValuesExcededError: Error {
var message: String
init(message: String) {
self.message = message
}
enum FilterError: Error {
case maxFilterValuesExceded(message: String)
case byNamesAndBySetsUsed(message: String)
}

class FilterBuilder {
private var filters = [SplitFilter]()
private let flagSetValidator: FlagSetsValidator

init(flagSetsValidator: FlagSetsValidator) {
self.flagSetValidator = flagSetsValidator
}

func add(filters: [SplitFilter]) -> Self {
self.filters.append(contentsOf: filters)
Expand All @@ -30,12 +33,28 @@ class FilterBuilder {
}

var groupedFilters = [SplitFilter]()
var filterCounts: [SplitFilter.FilterType: Int] = [:]
let types = Dictionary(grouping: filters, by: {$0.type}).keys
types.forEach { filterType in
let values = filters.filter({ $0.type == filterType }).flatMap({$0.values})
filterCounts[filterType] = values.count
groupedFilters.append(
SplitFilter(type: filterType, values: filters.filter({ $0.type == filterType }).flatMap({$0.values}))
SplitFilter(type: filterType, values: values)
)
}

if (filterCounts[.bySet] ?? 0) > 0,
((filterCounts[.byName] ?? 0) > 0 || (filterCounts[.byPrefix] ?? 0) > 0) {
let message = "SDK Config: names or prefix and sets filter cannot be used at the same time."
Logger.e(message)
}

// If bySets filter is present, ignore byNames and byPrefix
if let filter = groupedFilters.first(where: { $0.type == .bySet }) {
let values = flagSetValidator.cleanAndValidateValues(filter.values, calledFrom: "FilterBuilder.build")
return "&\(filter.type.queryStringField)=\(values.sorted().joined(separator: ","))"
}

groupedFilters.sort(by: { $0.type.rawValue < $1.type.rawValue})
var queryString = ""
for filter in groupedFilters {
Expand All @@ -50,12 +69,11 @@ class FilterBuilder {
. Please consider reducing the amount or using prefixes to target specific groups of feature flags.
"""
Logger.e(message)
throw MaxFilterValuesExcededError(message: message)
throw FilterError.maxFilterValuesExceded(message: message)
}
queryString.append("&\(filter.type.queryStringField)=\(deduptedValues.sorted().joined(separator: ","))")
}
return queryString

}

private func removeDuplicates(values: [String]) -> [String] {
Expand Down
66 changes: 66 additions & 0 deletions Split/Api/FlagSetsValidator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// FlagSetValidator.swift
// Split
//
// Created by Javier Avrudsky on 22/09/2023.
// Copyright © 2023 Split. All rights reserved.
//

import Foundation

protocol FlagSetsValidator {
func validateOnEvaluation(_ values: [String], calledFrom method: String, setsInFilter: [String]) -> [String]
func cleanAndValidateValues(_ values: [String], calledFrom method: String) -> [String]
}

struct DefaultFlagSetsValidator: FlagSetsValidator {

private var telemetryProducer: TelemetryInitProducer?

init(telemetryProducer: TelemetryInitProducer?) {
self.telemetryProducer = telemetryProducer
}

private let setRegex = "^[a-z0-9][a-z0-9_]{0,49}$"

func validateOnEvaluation(_ values: [String], calledFrom method: String, setsInFilter: [String]) -> [String] {
let filterSet = Set(setsInFilter)
return cleanAndValidateValues(values, calledFrom: method).filter { value in
if filterSet.count > 0, !filterSet.contains(value) {
Logger.w("\(method): you passed Flag Set: \(value) and is not part of " +
"the configured Flag set list, ignoring the request.")
return false
}
return true
}
}

func cleanAndValidateValues(_ values: [String], calledFrom method: String = "SDK Init") -> [String] {
var cleanSets = Set<String>()
for value in values {
let cleanValue = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if cleanValue.count < value.count {
Logger.w("\(method): Flag Set name <<\(value)>> has extra whitespace, trimming")
}
if !isValid(cleanValue) {
Logger.w("\(method): you passed \(cleanValue), Flag Set must adhere to the regular " +
"expressions \(setRegex). This means an Flag Set must be start with a letter, " +
"be in lowercase, alphanumeric and have a max length of 50 characters." +
"\(cleanValue) was discarded.")
continue
}
if !cleanSets.insert(cleanValue).inserted {
Logger.w("\(method): you passed duplicated Flag Set. \(cleanValue) was deduplicated.")
}
}
telemetryProducer?.recordTotalFlagSets(values.count)
telemetryProducer?.recordInvalidFlagSets(values.count - cleanSets.count)
return Array(cleanSets)
}

private func isValid(_ value: String) -> Bool {
let regex = try? NSRegularExpression(pattern: setRegex)
let range = NSRange(location: 0, length: value.utf16.count)
return regex?.numberOfMatches(in: value, options: [], range: range) ?? 0 > 0
}
}
19 changes: 19 additions & 0 deletions Split/Api/LocalhostSplitClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,22 @@ extension LocalhostSplitClient {
return true
}
}

// MARK: TreatmentBySets Feature
extension LocalhostSplitClient {
public func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String: String] {
return [String: String]()
}

public func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String: String] {
return [String: String]()
}

public func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String: SplitResult] {
return [String: SplitResult]()
}

public func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String: SplitResult] {
return [String: SplitResult]()
}
}
5 changes: 5 additions & 0 deletions Split/Api/SplitClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,9 @@ public typealias SplitAction = () -> Void
value: Double,
properties: [String: Any]?) -> Bool

// MARK: Evaluation with flagsets
func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: String]
func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: String]
func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String: Any]?) -> [String: SplitResult]
func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String: Any]?) -> [String: SplitResult]
}
17 changes: 17 additions & 0 deletions Split/Api/SplitClientConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,20 @@ public class SplitClientConfig: NSObject {
var cdnBackoffTimeMaxInSecs: Int = 60

}

extension SplitClientConfig {
func bySetsFilter() -> SplitFilter? {
// Group the filters by type
let groupedFilters = Dictionary(grouping: self.sync.filters) { $0.type }

// Extract and combine values for 'bySet' type
guard let sets = groupedFilters[.bySet]?.reduce(into: [String](), { result, filter in
result.append(contentsOf: filter.values)
}).sorted() else {
return nil
}

return SplitFilter(type: .bySet, values: sets)

}
}
Loading

0 comments on commit b3222fd

Please sign in to comment.