|
| 1 | +#!/usr/bin/env swift |
| 2 | +// |
| 3 | +// main.swift |
| 4 | +// |
| 5 | +// A command‐line tool to test Segment Analytics‑Swift SDK actions from a JSON file. |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import Segment |
| 10 | + |
| 11 | +var writeKey: String = "" |
| 12 | +var apiHost: String = "" |
| 13 | +var analytics: Analytics? = nil |
| 14 | + |
| 15 | +// MARK: - Action Handlers |
| 16 | + |
| 17 | +func processConfigure(_ action: [String: Any]) { |
| 18 | + print("writeKey: \(writeKey)") |
| 19 | + print("apihost: \(apiHost)") |
| 20 | + let config = Configuration(writeKey: writeKey) |
| 21 | + var waitUntilStarted = true |
| 22 | + |
| 23 | + // Set apiHost from command line argument or environment variable. May be overridden by test. |
| 24 | + if !apiHost.isEmpty { |
| 25 | + config.apiHost(apiHost) |
| 26 | + } |
| 27 | + |
| 28 | + for option in action { |
| 29 | + switch option.key { |
| 30 | + case "action", "writekey": |
| 31 | + // Ignore, already handled. |
| 32 | + continue |
| 33 | + case "flushAt": |
| 34 | + if let flushAt = option.value as? Int { |
| 35 | + config.flushAt(flushAt) |
| 36 | + } |
| 37 | + case "flushInterval": |
| 38 | + if let flushInterval = option.value as? TimeInterval { |
| 39 | + config.flushInterval(flushInterval) |
| 40 | + } |
| 41 | + case "trackApplicationLifecycleEvents": |
| 42 | + if let flag = option.value as? Bool { |
| 43 | + config.trackApplicationLifecycleEvents(flag) |
| 44 | + } |
| 45 | + case "autoAddSegmentDestination": |
| 46 | + if let flag = option.value as? Bool { |
| 47 | + config.autoAddSegmentDestination(flag) |
| 48 | + } |
| 49 | + case "apiHost": |
| 50 | + if let apiHost = option.value as? String { |
| 51 | + config.apiHost(apiHost) |
| 52 | + } |
| 53 | + case "cdnHost": |
| 54 | + if let cdnHost = option.value as? String { |
| 55 | + config.cdnHost(cdnHost) |
| 56 | + } |
| 57 | + case "operatingMode": |
| 58 | + if let modeString = option.value as? String { |
| 59 | + if modeString.lowercased() == "synchronous" { |
| 60 | + config.operatingMode(.synchronous) |
| 61 | + } else { |
| 62 | + config.operatingMode(.asynchronous) |
| 63 | + } |
| 64 | + } |
| 65 | + case "userAgent": |
| 66 | + if let userAgent = option.value as? String { |
| 67 | + config.userAgent(userAgent) |
| 68 | + } |
| 69 | + case "jsonNonConformingNumberStrategy": |
| 70 | + if let strategy = option.value as? String { |
| 71 | + switch strategy.lowercased() { |
| 72 | + case "zero": |
| 73 | + config.jsonNonConformingNumberStrategy(.zero) |
| 74 | + case "throw": |
| 75 | + config.jsonNonConformingNumberStrategy(.throw) |
| 76 | + case "null": |
| 77 | + config.jsonNonConformingNumberStrategy(.null) |
| 78 | + default: |
| 79 | + print("Unsupported jsonNonConformingNumberStrategy: \(strategy)") |
| 80 | + } |
| 81 | + } |
| 82 | + case "storageMode": |
| 83 | + if let modeValue = option.value as? String { |
| 84 | + switch modeValue.lowercased() { |
| 85 | + case "disk": |
| 86 | + config.storageMode(.disk) |
| 87 | + default: |
| 88 | + print("Unsupported storageMode string value: \(modeValue)") |
| 89 | + } |
| 90 | + } else if let memoryCount = option.value as? Int { |
| 91 | + config.storageMode(.memory(memoryCount)) |
| 92 | + } |
| 93 | + case "waitUntilStarted": |
| 94 | + // Not a config option but a behavior modification - default true, can set false for specific tests |
| 95 | + if let wait = option.value as? Bool { |
| 96 | + waitUntilStarted = wait |
| 97 | + } |
| 98 | + default: |
| 99 | + print("Unknown option: \(option.key)") |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + analytics = Analytics(configuration: config) |
| 104 | + if waitUntilStarted { |
| 105 | + analytics?.waitUntilStarted() |
| 106 | + } |
| 107 | + if let analytics = analytics { |
| 108 | + print("Configured analytics: \(analytics)") |
| 109 | + } else { |
| 110 | + print("Failed to configure analytics.") |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +// Process an 'identify' action. |
| 115 | +func processIdentify(_ action: [String: Any]) { |
| 116 | + guard let userId = action["userId"] as? String else { |
| 117 | + print("Missing userId in identify action.") |
| 118 | + return |
| 119 | + } |
| 120 | + // Optional traits dictionary. |
| 121 | + let traits = action["traits"] as? [String: Any] |
| 122 | + print("Identifying userId: \(userId)") |
| 123 | + analytics?.identify(userId: userId, traits: traits) |
| 124 | +} |
| 125 | + |
| 126 | +// Process a 'track' action. |
| 127 | +func processTrack(_ action: [String: Any]) { |
| 128 | + guard let event = action["event"] as? String else { |
| 129 | + print("Missing event in track action.") |
| 130 | + return |
| 131 | + } |
| 132 | + let properties = action["properties"] as? [String: Any] |
| 133 | + print("Tracking event: \(event)") |
| 134 | + analytics?.track(name: event, properties: properties) |
| 135 | +} |
| 136 | + |
| 137 | +// Process a 'screen' action. |
| 138 | +func processScreen(_ action: [String: Any]) { |
| 139 | + guard let name = action["name"] as? String else { |
| 140 | + print("Missing name in screen action.") |
| 141 | + return |
| 142 | + } |
| 143 | + let category = action["category"] as? String |
| 144 | + let properties = action["properties"] as? [String: Any] |
| 145 | + print("Screening with name: \(name)") |
| 146 | + analytics?.screen(title: name, category: category, properties: properties) |
| 147 | +} |
| 148 | + |
| 149 | +func processGroup(_ action: [String: Any]) { |
| 150 | + guard let groupId = action["groupId"] as? String else { |
| 151 | + print("Missing groupId in group action.") |
| 152 | + return |
| 153 | + } |
| 154 | + print("Grouping with groupId: \(groupId)") |
| 155 | + analytics?.group(groupId: groupId) |
| 156 | +} |
| 157 | + |
| 158 | +func processAlias(_ action: [String: Any]) { |
| 159 | + guard let alias = action["newId"] as? String else { |
| 160 | + print("Missing newId in alias action.") |
| 161 | + return |
| 162 | + } |
| 163 | + print("Alias to newId: \(alias)") |
| 164 | + analytics?.alias(newId: alias) |
| 165 | +} |
| 166 | + |
| 167 | +func processFlush(_ action: [String: Any]) { |
| 168 | + if let wait = action["wait"] as? Bool, wait { |
| 169 | + let semaphore = DispatchSemaphore(value: 0) |
| 170 | + analytics?.flush { |
| 171 | + semaphore.signal() |
| 172 | + } |
| 173 | + let timeout = DispatchTime.now() + .seconds(10) |
| 174 | + if semaphore.wait(timeout: timeout) == .timedOut { |
| 175 | + print("Flush timed out.") |
| 176 | + } |
| 177 | + print("Flush completed.") |
| 178 | + } else { |
| 179 | + analytics?.flush { |
| 180 | + print("Flush completed.") |
| 181 | + } |
| 182 | + print("Flush scheduled.") |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +func processEnabled(_ action: [String: Any]) { |
| 187 | + guard let enabled = action["enabled"] as? Bool else { |
| 188 | + print("Missing enabled in enabled action.") |
| 189 | + return |
| 190 | + } |
| 191 | + print("Setting enabled to: \(enabled)") |
| 192 | + analytics?.enabled = enabled |
| 193 | +} |
| 194 | + |
| 195 | +func processWait(_ action: [String: Any]) { |
| 196 | + guard let seconds = action["seconds"] as? Int else { |
| 197 | + print("Missing seconds in wait action.") |
| 198 | + return |
| 199 | + } |
| 200 | + print("Waiting for \(seconds) seconds.") |
| 201 | + sleep(UInt32(seconds)) |
| 202 | +} |
| 203 | + |
| 204 | +// Process one generic action according to its type. |
| 205 | +func processAction(_ action: [String: Any]) { |
| 206 | + guard let actionType = action["action"] as? String else { |
| 207 | + if let comment = action["comment"] as? String { |
| 208 | + print("Comment: \(comment)") |
| 209 | + } else { |
| 210 | + print("Missing action type in action: \(action)") |
| 211 | + } |
| 212 | + return |
| 213 | + } |
| 214 | + |
| 215 | + switch actionType.lowercased() { |
| 216 | + case "configure": |
| 217 | + processConfigure(action) |
| 218 | + case "identify": |
| 219 | + processIdentify(action) |
| 220 | + case "track": |
| 221 | + processTrack(action) |
| 222 | + case "screen", "page": |
| 223 | + processScreen(action) |
| 224 | + case "group": |
| 225 | + processGroup(action) |
| 226 | + case "alias": |
| 227 | + processAlias(action) |
| 228 | + case "flush": |
| 229 | + processFlush(action) |
| 230 | + case "enabled": |
| 231 | + processEnabled(action) |
| 232 | + case "reset": |
| 233 | + analytics?.reset() |
| 234 | + case "purgeStorage": |
| 235 | + analytics?.purgeStorage() |
| 236 | + case "waitUntilStarted": // Only useful if `waitUntilStarted` is set to false in the configuration. |
| 237 | + analytics?.waitUntilStarted() |
| 238 | + case "wait": |
| 239 | + processWait(action) |
| 240 | + default: |
| 241 | + print("Unknown action: \(actionType)") |
| 242 | + } |
| 243 | +} |
| 244 | + |
| 245 | +// MARK: - Main Program |
| 246 | +Telemetry.shared.enable = false |
| 247 | +Analytics.debugLogsEnabled = true |
| 248 | + |
| 249 | +// Ensure the JSON filename is passed as a command line argument. |
| 250 | +guard CommandLine.arguments.count >= 2 else { |
| 251 | + print("Usage: \(CommandLine.arguments[0]) path/to/actions.json [-wWRITEKEY] [-aAPIHOST]") |
| 252 | + exit(1) |
| 253 | +} |
| 254 | + |
| 255 | +// Get the JSON file path from the command line arguments. |
| 256 | +let jsonFilePath = CommandLine.arguments[1] |
| 257 | +let fileUrl = URL(fileURLWithPath: jsonFilePath) |
| 258 | + |
| 259 | +// Get the writeKey and apiHost from the command line arguments or environment variable. |
| 260 | +writeKey = "" |
| 261 | +apiHost = "" |
| 262 | +for argument in CommandLine.arguments { |
| 263 | + if argument.hasPrefix("-w") { |
| 264 | + writeKey = String(argument.dropFirst(2)) |
| 265 | + } else if argument.hasPrefix("-a") { |
| 266 | + apiHost = String(argument.dropFirst(2)) |
| 267 | + } |
| 268 | +} |
| 269 | + |
| 270 | +// Get the writeKey and apiHost from environment variables if not provided as command line arguments. |
| 271 | +if writeKey.isEmpty, let envWriteKey = ProcessInfo.processInfo.environment["E2E_WRITEKEY"] { |
| 272 | + writeKey = envWriteKey |
| 273 | +} |
| 274 | + |
| 275 | +if apiHost.isEmpty, let envApiHost = ProcessInfo.processInfo.environment["E2E_APIHOST"] { |
| 276 | + apiHost = envApiHost |
| 277 | +} |
| 278 | + |
| 279 | +if writeKey.isEmpty { |
| 280 | + print("Missing writeKey. Provide it as a command line argument with -wWRITEKEY or set the E2E_WRITEKEY environment variable.") |
| 281 | + exit(1) |
| 282 | +} |
| 283 | + |
| 284 | +do { |
| 285 | + let jsonData = try Data(contentsOf: fileUrl) |
| 286 | + // Expecting the JSON file to contain an array of actions: |
| 287 | + // [ |
| 288 | + // { "action": "identify", "userId": "user123", "traits": {"email": " [email protected]"} }, |
| 289 | + // { "action": "track", "event": "Item Purchased", "properties": {"item": "book", "price": 10} }, |
| 290 | + // { "action": "screen", "name": "Home", "category": "Landing", "properties": {"title": "Welcome"} }, |
| 291 | + // { "action": "group", "groupId": "group123" } |
| 292 | + // ] |
| 293 | + guard |
| 294 | + let jsonArray = try JSONSerialization.jsonObject(with: jsonData, options: []) |
| 295 | + as? [[String: Any]] |
| 296 | + else { |
| 297 | + print("JSON file does not contain an array of actions") |
| 298 | + exit(1) |
| 299 | + } |
| 300 | + |
| 301 | + // Process each action in the order received. |
| 302 | + for action in jsonArray { |
| 303 | + processAction(action) |
| 304 | + // Optionally flush after each action if needed: |
| 305 | + // Analytics.sharedInstance.flush() |
| 306 | + } |
| 307 | + |
| 308 | +} catch { |
| 309 | + print("Error reading or parsing the JSON file: \(error)") |
| 310 | + exit(1) |
| 311 | +} |
| 312 | + |
| 313 | +print("All actions processed.") |
| 314 | +// End of main.swift |
0 commit comments