Skip to content

Commit ff63cf6

Browse files
bsneedBrandon Sneed
andauthored
Made working with and modifying payloads within plugins much simpler. (#83)
* First pass conveniences * Updated gitignore * Made logging available outside an analytics instance. * Added convenience methods for modifying integrations/context * Added subscripts and methods for value access/setting * Reordered keypath handlers such that the most-used is seen first. * Added convenience class to flatten multi-wrapped optionals. * Added unit tests for previous changes. * Reverted keypath handler change. * Removed extraneous file * Added header doc for manually enabling plugins. * Added docs around throws for some methods. * Fixed example project. Co-authored-by: Brandon Sneed <brandon.sneed@segment.com>
1 parent 78325d3 commit ff63cf6

File tree

11 files changed

+609
-38
lines changed

11 files changed

+609
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,4 @@ iOSInjectionProject/
9191
.DS_Store
9292
Package.resolved
9393
*.xcuserdatad
94+
/.swiftpm/xcode/xcshareddata

Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
46871694270E16080028B595 /* NotificationTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4687168E270E16080028B595 /* NotificationTracking.swift */; };
11+
46871695270E16080028B595 /* UIKitScreenTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4687168F270E16080028B595 /* UIKitScreenTracking.swift */; };
12+
46871696270E16080028B595 /* ConsentTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46871690270E16080028B595 /* ConsentTracking.swift */; };
13+
46871697270E16080028B595 /* IDFACollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46871691270E16080028B595 /* IDFACollection.swift */; };
14+
46871698270E16080028B595 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46871692270E16080028B595 /* ConsoleLogger.swift */; };
1015
469EC8D0266066130068F9E3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 469EC8CF266066130068F9E3 /* SystemConfiguration.framework */; };
1116
469EC8E0266828860068F9E3 /* FlurryAnalyticsSPM in Frameworks */ = {isa = PBXBuildFile; productRef = 469EC8DF266828860068F9E3 /* FlurryAnalyticsSPM */; };
1217
469F7B08266011690038E773 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 469F7B07266011690038E773 /* AppDelegate.swift */; };
@@ -36,6 +41,11 @@
3641
/* End PBXBuildFile section */
3742

3843
/* Begin PBXFileReference section */
44+
4687168E270E16080028B595 /* NotificationTracking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTracking.swift; sourceTree = "<group>"; };
45+
4687168F270E16080028B595 /* UIKitScreenTracking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitScreenTracking.swift; sourceTree = "<group>"; };
46+
46871690270E16080028B595 /* ConsentTracking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsentTracking.swift; sourceTree = "<group>"; };
47+
46871691270E16080028B595 /* IDFACollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IDFACollection.swift; sourceTree = "<group>"; };
48+
46871692270E16080028B595 /* ConsoleLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
3949
469EC8CF266066130068F9E3 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
4050
469EC8E1266828AF0068F9E3 /* DestinationsExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DestinationsExample-Bridging-Header.h"; sourceTree = "<group>"; };
4151
469F7B04266011690038E773 /* DestinationsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DestinationsExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -80,6 +90,19 @@
8090
/* End PBXFrameworksBuildPhase section */
8191

8292
/* Begin PBXGroup section */
93+
4687168C270E16080028B595 /* other_plugins */ = {
94+
isa = PBXGroup;
95+
children = (
96+
4687168E270E16080028B595 /* NotificationTracking.swift */,
97+
4687168F270E16080028B595 /* UIKitScreenTracking.swift */,
98+
46871690270E16080028B595 /* ConsentTracking.swift */,
99+
46871691270E16080028B595 /* IDFACollection.swift */,
100+
46871692270E16080028B595 /* ConsoleLogger.swift */,
101+
);
102+
name = other_plugins;
103+
path = ../../../other_plugins;
104+
sourceTree = "<group>";
105+
};
83106
469F7AFB266011690038E773 = {
84107
isa = PBXGroup;
85108
children = (
@@ -101,6 +124,7 @@
101124
isa = PBXGroup;
102125
children = (
103126
BA384C9D2686609000AFEA1B /* DestinationsExample.entitlements */,
127+
4687168C270E16080028B595 /* other_plugins */,
104128
469F7B1E266012CB0038E773 /* destination_plugins */,
105129
469F7B07266011690038E773 /* AppDelegate.swift */,
106130
469F7B09266011690038E773 /* SceneDelegate.swift */,
@@ -241,6 +265,7 @@
241265
buildActionMask = 2147483647;
242266
files = (
243267
BA384C9A2682973300AFEA1B /* AppsFlyerDestination.swift in Sources */,
268+
46871698270E16080028B595 /* ConsoleLogger.swift in Sources */,
244269
469F7B20266012CB0038E773 /* FlurryDestination.swift in Sources */,
245270
469F7B0C266011690038E773 /* ViewController.swift in Sources */,
246271
96469A9B270279A600AC5772 /* IntercomDestination.swift in Sources */,
@@ -249,9 +274,13 @@
249274
96DBF37D26FA943300724B0B /* ComscoreDestination.swift in Sources */,
250275
965DC0FB2668077400DDF9C7 /* AmplitudeSession.swift in Sources */,
251276
469F7B08266011690038E773 /* AppDelegate.swift in Sources */,
277+
46871694270E16080028B595 /* NotificationTracking.swift in Sources */,
252278
469F7B25266013320038E773 /* AdjustDestination.swift in Sources */,
253279
469F7B0A266011690038E773 /* SceneDelegate.swift in Sources */,
254280
965DC1232669947F00DDF9C7 /* FirebaseDestination.swift in Sources */,
281+
46871695270E16080028B595 /* UIKitScreenTracking.swift in Sources */,
282+
46871696270E16080028B595 /* ConsentTracking.swift in Sources */,
283+
46871697270E16080028B595 /* IDFACollection.swift in Sources */,
255284
);
256285
runOnlyForDeploymentPostprocessing = 0;
257286
};

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/Segment/Plugins/Logger/SegmentLog.swift

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,32 @@ internal class SegmentLog: UtilityPlugin {
2020

2121
// For internal use only. Note: This will contain the last created instance
2222
// of analytics when used in a multi-analytics environment.
23-
internal static var sharedAnalytics: Analytics?
23+
internal static var sharedAnalytics: Analytics? = nil
24+
25+
#if DEBUG
26+
internal static var globalLogger: SegmentLog {
27+
get {
28+
let logger = SegmentLog()
29+
logger.addTargets()
30+
return logger
31+
}
32+
}
33+
#endif
2434

2535
required init() { }
2636

2737
func configure(analytics: Analytics) {
2838
self.analytics = analytics
2939
SegmentLog.sharedAnalytics = analytics
40+
addTargets()
41+
}
42+
43+
internal func addTargets() {
3044
#if !os(Linux)
3145
try? add(target: SystemTarget(), for: LoggingType.log)
46+
#if DEBUG
47+
try? add(target: ConsoleTarget(), for: LoggingType.log)
48+
#endif
3249
#else
3350
try? add(target: ConsoleTarget(), for: LoggingType.log)
3451
#endif
@@ -146,16 +163,23 @@ internal extension Analytics {
146163
/// - function: The name of the function the log came from. This will be captured automatically.
147164
/// - line: The line number in the function the log came from. This will be captured automatically.
148165
static func segmentLog(message: String, kind: LogFilterKind? = nil, function: String = #function, line: Int = #line) {
149-
SegmentLog.sharedAnalytics?.apply { plugin in
150-
if let loggerPlugin = plugin as? SegmentLog {
151-
var filterKind = loggerPlugin.filterKind
152-
if let logKind = kind {
153-
filterKind = logKind
166+
if let shared = SegmentLog.sharedAnalytics {
167+
shared.apply { plugin in
168+
if let loggerPlugin = plugin as? SegmentLog {
169+
var filterKind = loggerPlugin.filterKind
170+
if let logKind = kind {
171+
filterKind = logKind
172+
}
173+
174+
let log = LogFactory.buildLog(destination: .log, title: "", message: message, kind: filterKind, function: function, line: line)
175+
loggerPlugin.log(log, destination: .log)
154176
}
155-
156-
let log = LogFactory.buildLog(destination: .log, title: "", message: message, kind: filterKind, function: function, line: line)
157-
loggerPlugin.log(log, destination: .log)
158177
}
178+
} else {
179+
#if DEBUG
180+
let log = LogFactory.buildLog(destination: .log, title: "", message: message, kind: .debug, function: function, line: line)
181+
SegmentLog.globalLogger.log(log, destination: .log)
182+
#endif
159183
}
160184
}
161185

Sources/Segment/Settings.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ extension Settings: Equatable {
8686
}
8787

8888
extension Analytics {
89+
/// Manually enable a destination plugin. This is useful when a given DestinationPlugin doesn't have any Segment tie-ins at all.
90+
/// This will allow the destination to be processed in the same way within this library.
91+
/// - Parameters:
92+
/// - plugin: The destination plugin to enable.
93+
public func manuallyEnableDestination(plugin: DestinationPlugin) {
94+
self.store.dispatch(action: System.AddIntegrationAction(key: plugin.key))
95+
}
96+
8997
internal func update(settings: Settings, type: UpdateType) {
9098
apply { (plugin) in
9199
// tell all top level plugins to update.

Sources/Segment/Types.swift

Lines changed: 183 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,10 @@ import Foundation
99
import Sovran
1010

1111

12-
// MARK: - Event Parameter Types
13-
14-
typealias Integrations = Codable
15-
typealias Properties = Codable
16-
typealias Traits = Codable
17-
18-
1912
// MARK: - Event Types
2013

2114
public protocol RawEvent: Codable {
15+
2216
var type: String? { get set }
2317
var anonymousId: String? { get set }
2418
var messageId: String? { get set }
@@ -149,11 +143,192 @@ public struct AliasEvent: RawEvent {
149143
}
150144
}
151145

146+
// MARK: - RawEvent conveniences
147+
148+
internal struct IntegrationConstants {
149+
static let allIntegrationsKey = "All"
150+
}
151+
152+
extension RawEvent {
153+
/**
154+
Disable all cloud-mode integrations for this event, except for any specific keys given.
155+
This will preserve any per-integration specific settings if the integration is to remain enabled.
156+
- Parameters:
157+
- exceptKeys: A list of integration keys to exclude from disabling.
158+
*/
159+
public mutating func disableCloudIntegrations(exceptKeys: [String]? = nil) {
160+
guard let existing = integrations?.dictionaryValue else {
161+
// this shouldn't happen, might oughta log it.
162+
Analytics.segmentLog(message: "Unable to get what should be a valid list of integrations from event.", kind: .error)
163+
return
164+
}
165+
var new = [String: Any]()
166+
new[IntegrationConstants.allIntegrationsKey] = false
167+
if let exceptKeys = exceptKeys {
168+
for key in exceptKeys {
169+
if let value = existing[key], value is [String: Any] {
170+
new[key] = value
171+
} else {
172+
new[key] = true
173+
}
174+
}
175+
}
176+
177+
do {
178+
integrations = try JSON(new)
179+
} catch {
180+
// this shouldn't happen, log it.
181+
Analytics.segmentLog(message: "Unable to convert list of integrations to JSON. \(error)", kind: .error)
182+
}
183+
}
184+
185+
/**
186+
Enable all cloud-mode integrations for this event, except for any specific keys given.
187+
- Parameters:
188+
- exceptKeys: A list of integration keys to exclude from enabling.
189+
*/
190+
public mutating func enableCloudIntegrations(exceptKeys: [String]? = nil) {
191+
var new = [String: Any]()
192+
new[IntegrationConstants.allIntegrationsKey] = true
193+
if let exceptKeys = exceptKeys {
194+
for key in exceptKeys {
195+
new[key] = false
196+
}
197+
}
198+
199+
do {
200+
integrations = try JSON(new)
201+
} catch {
202+
// this shouldn't happen, log it.
203+
Analytics.segmentLog(message: "Unable to convert list of integrations to JSON. \(error)", kind: .error)
204+
}
205+
}
206+
207+
/**
208+
Disable a specific cloud-mode integration using it's key name.
209+
- Parameters:
210+
- key: The key name of the integration to disable.
211+
*/
212+
public mutating func disableIntegration(key: String) {
213+
guard let existing = integrations?.dictionaryValue else {
214+
// this shouldn't happen, might oughta log it.
215+
Analytics.segmentLog(message: "Unable to get what should be a valid list of integrations from event.", kind: .error)
216+
return
217+
}
218+
// we don't really care what the value of this key was before, as
219+
// a disabled one can only be false.
220+
var new = existing
221+
new[key] = false
222+
223+
do {
224+
integrations = try JSON(new)
225+
} catch {
226+
// this shouldn't happen, log it.
227+
Analytics.segmentLog(message: "Unable to convert list of integrations to JSON. \(error)", kind: .error)
228+
}
229+
}
230+
231+
/**
232+
Enable a specific cloud-mode integration using it's key name.
233+
- Parameters:
234+
- key: The key name of the integration to enable.
235+
*/
236+
public mutating func enableIntegration(key: String) {
237+
guard let existing = integrations?.dictionaryValue else {
238+
// this shouldn't happen, might oughta log it.
239+
Analytics.segmentLog(message: "Unable to get what should be a valid list of integrations from event.", kind: .error)
240+
return
241+
}
242+
243+
var new = existing
244+
// if it's a dictionary already, it's considered enabled, so don't
245+
// overwrite whatever they may have put there. If that's not the case
246+
// just set it to true since that's the only other value it could have
247+
// to be considered `enabled`.
248+
if (existing[key] as? [String: Any]) == nil {
249+
new[key] = true
250+
}
251+
252+
do {
253+
integrations = try JSON(new)
254+
} catch {
255+
// this shouldn't happen, log it.
256+
Analytics.segmentLog(message: "Unable to convert list of integrations to JSON. \(error)", kind: .error)
257+
}
258+
}
259+
260+
/**
261+
Set values to be received for this event in cloud-mode, specific to an integration key path.
262+
Note that when specifying nil as the value, the key will be removed for the given key path. Additionally,
263+
any keys that don't already exist in the path will be created as necessary.
264+
265+
Example:
266+
```
267+
trackEvent.setIntegrationValue(42, forKeyPath: "Amplitude.threshold")
268+
```
269+
270+
- Parameters:
271+
- value: The value to set for the given keyPath, or nil.
272+
- forKeyPath: The key path for the value.
273+
*/
274+
public mutating func setIntegrationValue(_ value: Any?, forKeyPath keyPath: String) {
275+
guard let existing = integrations?.dictionaryValue else {
276+
// this shouldn't happen, might oughta log it.
277+
Analytics.segmentLog(message: "Unable to get what should be a valid list of integrations from event.", kind: .error)
278+
return
279+
}
280+
281+
var new = existing
282+
new[keyPath: KeyPath(keyPath)] = value
283+
do {
284+
integrations = try JSON(new)
285+
} catch {
286+
// this shouldn't happen, log it.
287+
Analytics.segmentLog(message: "Unable to convert list of integrations to JSON. \(error)", kind: .error)
288+
}
289+
}
290+
291+
/**
292+
Set context values for this event.
293+
Note that when specifying nil as the value, the key will be removed for the given key path. Additionally,
294+
any keys that don't already exist in the path will be created as necessary.
295+
296+
Example:
297+
```
298+
// the metadata key will be created as a dictionary, and the key nickname will be set.
299+
trackEvent.setContextValue("Brandon's device", forKeyPath: "device.metadata.nickname")
300+
301+
// the metadata key will be removed entirely.
302+
trackEvent.setContextValue(nil, forKeyPath: "device.metadata")
303+
```
304+
305+
- Parameters:
306+
- value: The value to set for the given keyPath, or nil.
307+
- forKeyPath: The key path for the value.
308+
*/
309+
public mutating func setContextValue(_ value: Any?, forKeyPath keyPath: String) {
310+
guard let existing = context?.dictionaryValue else {
311+
// this shouldn't happen, might oughta log it.
312+
Analytics.segmentLog(message: "Unable to get what should be a valid context from event.", kind: .error)
313+
return
314+
}
315+
316+
var new = existing
317+
new[keyPath: KeyPath(keyPath)] = value
318+
do {
319+
context = try JSON(new)
320+
} catch {
321+
// this shouldn't happen, log it.
322+
Analytics.segmentLog(message: "Unable to convert context to JSON. \(error)", kind: .error)
323+
}
324+
}
325+
}
326+
152327

153328
// MARK: - RawEvent data helpers
154329

155330
extension RawEvent {
156-
public mutating func applyRawEventData(event: RawEvent?) {
331+
internal mutating func applyRawEventData(event: RawEvent?) {
157332
if let e = event {
158333
anonymousId = e.anonymousId
159334
messageId = e.messageId

0 commit comments

Comments
 (0)