From 8ab992f027d91cf66ab3801638c63e13776e13e1 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 26 Jun 2025 12:48:45 -0700 Subject: [PATCH 1/2] Improved debug apparatus --- .../Broadcasters/SegmentBroadcaster.swift | 28 +++--- .../AnalyticsLive/Signals/Configuration.swift | 47 ++++++---- .../Signals/Minilytics/MiniAnalytics.swift | 13 ++- Sources/AnalyticsLive/Signals/Signals.swift | 14 +-- .../Signals/Utilities/Obfuscation.swift | 7 -- .../Signals/SignalsTests.swift | 86 +++++++++++++++++++ 6 files changed, 144 insertions(+), 51 deletions(-) diff --git a/Sources/AnalyticsLive/Signals/Broadcasters/SegmentBroadcaster.swift b/Sources/AnalyticsLive/Signals/Broadcasters/SegmentBroadcaster.swift index d216df3..28cd304 100755 --- a/Sources/AnalyticsLive/Signals/Broadcasters/SegmentBroadcaster.swift +++ b/Sources/AnalyticsLive/Signals/Broadcasters/SegmentBroadcaster.swift @@ -11,26 +11,32 @@ import Segment public class SegmentBroadcaster: SignalBroadcaster { public weak var analytics: Analytics? = nil { didSet { - #if DEBUG - guard let analytics else { return } - self.mini = MiniAnalytics(analytics: analytics) - #endif + if sendToSegment { + guard let analytics else { return } + self.mini = MiniAnalytics(analytics: analytics) + } } } + internal let sendToSegment: Bool + internal let obfuscate: Bool internal var mini: MiniAnalytics? = nil public func added(signal: any RawSignal) { - #if DEBUG - mini?.track(signal: signal) - #endif + var s = signal + if sendToSegment { + mini?.track(signal: s, obfuscate: obfuscate) + } } public func relay() { - #if DEBUG - mini?.flush() - #endif + if sendToSegment { + mini?.flush() + } } - public init() { } + public init(sendToSegment: Bool = false, obfuscate: Bool = true) { + self.obfuscate = obfuscate + self.sendToSegment = sendToSegment + } } diff --git a/Sources/AnalyticsLive/Signals/Configuration.swift b/Sources/AnalyticsLive/Signals/Configuration.swift index 6c5ff1c..e696bcc 100755 --- a/Sources/AnalyticsLive/Signals/Configuration.swift +++ b/Sources/AnalyticsLive/Signals/Configuration.swift @@ -19,23 +19,27 @@ public struct SignalsConfiguration { "signals.segment.build", ] - public let writeKey: String - public let maximumBufferSize: Int - public let relayCount: Int - public let relayInterval: TimeInterval - public let broadcasters: [SignalBroadcaster]? - public let useUIKitAutoSignal: Bool - public let useSwiftUIAutoSignal: Bool - public let useNetworkAutoSignal: Bool - public let allowedNetworkHosts: [String] - public let blockedNetworkHosts: [String] + internal let writeKey: String + internal let maximumBufferSize: Int + internal let relayCount: Int + internal let relayInterval: TimeInterval + internal var broadcasters: [SignalBroadcaster] + internal let sendDebugSignalsToSegment: Bool + internal let obfuscateDebugSignals: Bool + internal let useUIKitAutoSignal: Bool + internal let useSwiftUIAutoSignal: Bool + internal let useNetworkAutoSignal: Bool + internal let allowedNetworkHosts: [String] + internal let blockedNetworkHosts: [String] public init( writeKey: String, maximumBufferSize: Int = 1000, relayCount: Int = 20, relayInterval: TimeInterval = 60, - broadcasters: [SignalBroadcaster]? = [SegmentBroadcaster()], + broadcasters: [SignalBroadcaster] = [], + sendDebugSignalsToSegment: Bool = false, + obfuscateDebugSignals: Bool = true, useUIKitAutoSignal: Bool = false, useSwiftUIAutoSignal: Bool = false, useNetworkAutoSignal: Bool = false, @@ -47,20 +51,31 @@ public struct SignalsConfiguration { self.relayCount = relayCount self.relayInterval = relayInterval self.broadcasters = broadcasters + self.sendDebugSignalsToSegment = sendDebugSignalsToSegment + self.obfuscateDebugSignals = obfuscateDebugSignals self.useUIKitAutoSignal = useUIKitAutoSignal self.useSwiftUIAutoSignal = useSwiftUIAutoSignal self.useNetworkAutoSignal = useNetworkAutoSignal self.allowedNetworkHosts = allowedNetworkHosts + if !self.broadcasters.contains(where: { $0 is SegmentBroadcaster }) { + if self.sendDebugSignalsToSegment { + let seg = SegmentBroadcaster( + sendToSegment: self.sendDebugSignalsToSegment, + obfuscate: self.obfuscateDebugSignals + ) + self.broadcasters.append(seg) + } + } + var blocked = blockedNetworkHosts + Self.autoBlockedHosts // block the webhook if it's in use - if let broadcasters = self.broadcasters { - for b in broadcasters { - if let webhook = b as? WebhookBroadcaster, let host = webhook.webhookURL.host() { - blocked.append(host) - } + for b in self.broadcasters { + if let webhook = b as? WebhookBroadcaster, let host = webhook.webhookURL.host() { + blocked.append(host) } } + self.blockedNetworkHosts = blocked } } diff --git a/Sources/AnalyticsLive/Signals/Minilytics/MiniAnalytics.swift b/Sources/AnalyticsLive/Signals/Minilytics/MiniAnalytics.swift index b2f3218..b827562 100755 --- a/Sources/AnalyticsLive/Signals/Minilytics/MiniAnalytics.swift +++ b/Sources/AnalyticsLive/Signals/Minilytics/MiniAnalytics.swift @@ -61,6 +61,8 @@ internal class MiniAnalytics { let storage: TransientDB @Atomic var flushing: Bool = false + // used for testing only. + internal static var observer: ((_ in: any RawSignal, _ out: MiniTrackEvent) -> Void)? = nil init(analytics: Analytics) { self.analytics = analytics @@ -78,14 +80,13 @@ internal class MiniAnalytics { self.storage = TransientDB(store: fileStore, asyncAppend: true) } - func track(signal: any RawSignal) { + func track(signal: any RawSignal, obfuscate: Bool) { + var input = signal var signal = signal - #if !DEBUG - if let obf = signal as? JSONObfuscation { + if obfuscate, let obf = signal as? JSONObfuscation { signal = obf.obfuscated() } - #endif guard let props = try? JSON(with: signal) else { return } @@ -102,6 +103,10 @@ internal class MiniAnalytics { properties: props) storage.append(data: track) + + if let observer = Self.observer { + observer(input, track) + } } func flush() { diff --git a/Sources/AnalyticsLive/Signals/Signals.swift b/Sources/AnalyticsLive/Signals/Signals.swift index 341b31f..72b1fff 100755 --- a/Sources/AnalyticsLive/Signals/Signals.swift +++ b/Sources/AnalyticsLive/Signals/Signals.swift @@ -85,7 +85,6 @@ public class Signals: Plugin, LivePluginsDependent { public func useConfiguration(_ configuration: SignalsConfiguration) { _configuration.set(configuration) - addDefaultBroadcasters() updateJSConfiguration() updateNativeConfiguration() @@ -239,23 +238,12 @@ extension Signals { } } - internal func addDefaultBroadcasters() { - if let cb = configuration.broadcasters { - broadcasters = cb - } - - if !broadcasters.contains(where: { broadcaster in - return broadcaster is SegmentBroadcaster - }) { - broadcasters.append(SegmentBroadcaster()) - } - } - internal func updateJSConfiguration() { signalObject?.setValue(configuration.maximumBufferSize, for: "maxBufferSize") } internal func updateNativeConfiguration() { + broadcasters = configuration.broadcasters broadcastTimer = QueueTimer(interval: configuration.relayInterval, handler: { [weak self] in guard let self else { return } for b in self.broadcasters { b.relay() } diff --git a/Sources/AnalyticsLive/Signals/Utilities/Obfuscation.swift b/Sources/AnalyticsLive/Signals/Utilities/Obfuscation.swift index cbf9236..4676c86 100755 --- a/Sources/AnalyticsLive/Signals/Utilities/Obfuscation.swift +++ b/Sources/AnalyticsLive/Signals/Utilities/Obfuscation.swift @@ -46,13 +46,6 @@ extension NetworkSignal: JSONObfuscation { extension JSONObfuscation { func obfuscate(_ data: JSON?) -> JSON? { - #if DEBUG - let debugging = true - #else - let debugging = false - #endif - if debugging { return data } - guard let data else { return data } switch data { diff --git a/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift b/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift index bc01c4a..8c000c2 100755 --- a/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift +++ b/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift @@ -1,3 +1,89 @@ import XCTest +import Segment @testable import AnalyticsLive +final class TestSignals: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testSendToSegment() throws { + LivePlugins.clearCache() + + let config = Configuration(writeKey: "signals_test") + .flushInterval(999999999) + .flushAt(99999999) + let analytics = Analytics(configuration: config) + + let signalsConfig = SignalsConfiguration( + writeKey: "signals_test", + sendDebugSignalsToSegment: true + ) + + // set up an observer. + let expectation = self.expectation(description: "observer called") + MiniAnalytics.observer = { signal, event in + print("signal: \(signal.prettyPrint())") + print("event: \(event.prettyPrint())") + + XCTAssertEqual(event.properties.value(forKeyPath: "data.data.customer_name"), "XXXX XXX") + XCTAssertEqual(event.properties.value(forKeyPath: "data.data.price"), "99.99") + expectation.fulfill() + } + Signals.shared.useConfiguration(signalsConfig) + analytics.add(plugin: LivePlugins(fallbackFileURL: bundleTestFile(file: "MyEdgeFunctions.js"))) + analytics.add(plugin: Signals.shared) + + analytics.waitUntilStarted() + + let localData = LocalDataSignal(action: .loaded, identifier: "1234", data: ["price": "19.95", "customer_name": "John Doe"]) + Signals.emit(signal: localData) + + waitForExpectations(timeout: 5) { error in + + } + } + + func testSendToSegmentUnobfuscated() throws { + LivePlugins.clearCache() + + let config = Configuration(writeKey: "signals_test2") + .flushInterval(999999999) + .flushAt(99999999) + let analytics = Analytics(configuration: config) + + let signalsConfig = SignalsConfiguration( + writeKey: "signals_test2", + sendDebugSignalsToSegment: true, + obfuscateDebugSignals: false + ) + + // set up an observer. + let expectation = self.expectation(description: "observer called") + MiniAnalytics.observer = { signal, event in + print("signal: \(signal.prettyPrint())") + print("event: \(event.prettyPrint())") + + XCTAssertEqual(event.properties.value(forKeyPath: "data.data.customer_name"), "John Doe") + XCTAssertEqual(event.properties.value(forKeyPath: "data.data.price"), "19.95") + expectation.fulfill() + } + Signals.shared.useConfiguration(signalsConfig) + analytics.add(plugin: LivePlugins(fallbackFileURL: bundleTestFile(file: "MyEdgeFunctions.js"))) + analytics.add(plugin: Signals.shared) + + analytics.waitUntilStarted() + + let localData = LocalDataSignal(action: .loaded, identifier: "1234", data: ["price": "19.95", "customer_name": "John Doe"]) + Signals.emit(signal: localData) + + waitForExpectations(timeout: 5) { error in + + } + } +} From 7fcb61af2d71ca2b5b9202d97057627a0b059ddb Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 26 Jun 2025 13:10:08 -0700 Subject: [PATCH 2/2] Fixed empty wait. --- Tests/AnalyticsLiveTests/Signals/SignalsTests.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift b/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift index 8c000c2..248e4bc 100755 --- a/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift +++ b/Tests/AnalyticsLiveTests/Signals/SignalsTests.swift @@ -44,9 +44,7 @@ final class TestSignals: XCTestCase { let localData = LocalDataSignal(action: .loaded, identifier: "1234", data: ["price": "19.95", "customer_name": "John Doe"]) Signals.emit(signal: localData) - waitForExpectations(timeout: 5) { error in - - } + waitForExpectations(timeout: 5) } func testSendToSegmentUnobfuscated() throws { @@ -82,8 +80,6 @@ final class TestSignals: XCTestCase { let localData = LocalDataSignal(action: .loaded, identifier: "1234", data: ["price": "19.95", "customer_name": "John Doe"]) Signals.emit(signal: localData) - waitForExpectations(timeout: 5) { error in - - } + waitForExpectations(timeout: 5) } }