Skip to content

Commit 249f2a9

Browse files
authored
Capture Destination Metadata (#116)
* capture destination metadata * add writeKey to batch files and switch to /b endpoint
1 parent 76316a5 commit 249f2a9

File tree

7 files changed

+131
-36
lines changed

7 files changed

+131
-36
lines changed

Segment.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
46FE4CE025A53FAD003A7362 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4CDF25A53FAD003A7362 /* Storage.swift */; };
4949
46FE4CFB25A6C671003A7362 /* TestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4CFA25A6C671003A7362 /* TestUtilities.swift */; };
5050
46FE4D1D25A7A850003A7362 /* Storage_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4D1C25A7A850003A7362 /* Storage_Tests.swift */; };
51+
759D6CD127B48ABB00AB900A /* DestinationMetadataPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759D6CD027B48ABB00AB900A /* DestinationMetadataPlugin.swift */; };
5152
9620862C2575C0C800314F8D /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620862B2575C0C800314F8D /* Events.swift */; };
5253
96208650257AA83E00314F8D /* iOSLifecycleMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */; };
5354
96259F8326CEF526008AE301 /* LogTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96259F8226CEF526008AE301 /* LogTarget.swift */; };
@@ -147,6 +148,7 @@
147148
46FE4CDF25A53FAD003A7362 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
148149
46FE4CFA25A6C671003A7362 /* TestUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtilities.swift; sourceTree = "<group>"; };
149150
46FE4D1C25A7A850003A7362 /* Storage_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage_Tests.swift; sourceTree = "<group>"; };
151+
759D6CD027B48ABB00AB900A /* DestinationMetadataPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationMetadataPlugin.swift; sourceTree = "<group>"; };
150152
9620862B2575C0C800314F8D /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
151153
962086482579CCC200314F8D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
152154
9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLifecycleMonitor.swift; sourceTree = "<group>"; };
@@ -293,6 +295,7 @@
293295
9692726725A583A6009B5298 /* SegmentDestination.swift */,
294296
46210835260BBEE400EBC4A8 /* DeviceToken.swift */,
295297
46031D64266E7C10009BA540 /* StartupQueue.swift */,
298+
759D6CD027B48ABB00AB900A /* DestinationMetadataPlugin.swift */,
296299
);
297300
path = Plugins;
298301
sourceTree = "<group>";
@@ -562,6 +565,7 @@
562565
460227422612987300A9E913 /* watchOSLifecycleEvents.swift in Sources */,
563566
96259F8326CEF526008AE301 /* LogTarget.swift in Sources */,
564567
46F7485E26C718710042798E /* ObjCConfiguration.swift in Sources */,
568+
759D6CD127B48ABB00AB900A /* DestinationMetadataPlugin.swift in Sources */,
565569
A31A162F2576B73F00C9CDDF /* State.swift in Sources */,
566570
9692726825A583A6009B5298 /* SegmentDestination.swift in Sources */,
567571
4602276C261E7BF900A9E913 /* iOSDelegation.swift in Sources */,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// DestinationMetadataPlugin.swift
3+
// Segment
4+
//
5+
// Created by Prayansh Srivastava on 2/9/22.
6+
//
7+
8+
import Foundation
9+
10+
/**
11+
* DestinationMetadataPlugin adds `_metadata` information to payloads that Segment uses to
12+
* determine delivery of events to cloud/device-mode destinations
13+
*/
14+
public class DestinationMetadataPlugin: Plugin {
15+
public let type: PluginType = PluginType.enrichment
16+
public var analytics: Analytics?
17+
private var analyticsSettings: Settings? = nil
18+
19+
public func update(settings: Settings, type: UpdateType) {
20+
analyticsSettings = settings
21+
}
22+
23+
public func execute<T: RawEvent>(event: T?) -> T? {
24+
guard var modified = event else {
25+
return event
26+
}
27+
28+
guard let integrationSettings = analytics?.settings() else { return event }
29+
guard let destinations = analytics?.timeline.plugins[.destination]?.plugins as? [DestinationPlugin] else { return event }
30+
31+
// Mark all loaded and enabled destinations as bundled
32+
var bundled = [String]()
33+
for plugin in destinations {
34+
// Skip processing for Segment.io
35+
if (plugin is SegmentDestination) {
36+
continue
37+
}
38+
let hasSettings = integrationSettings.hasIntegrationSettings(forPlugin: plugin)
39+
if hasSettings {
40+
// we have a device mode plugin installed.
41+
bundled.append(plugin.key)
42+
}
43+
}
44+
45+
// All unbundledIntegrations not in `bundled` are put in `unbundled`
46+
var unbundled = [String]()
47+
let segmentInfo = integrationSettings.integrationSettings(forKey: "Segment.io")
48+
let unbundledIntegrations = segmentInfo?["unbundledIntegrations"] as? [String] ?? []
49+
for integration in unbundledIntegrations {
50+
if (!bundled.contains(integration)) {
51+
unbundled.append(integration)
52+
}
53+
}
54+
55+
modified._metadata = DestinationMetadata(bundled: bundled, unbundled: unbundled, bundledIds: [])
56+
57+
return modified
58+
}
59+
}

Sources/Segment/Plugins/SegmentDestination.swift

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public class SegmentDestination: DestinationPlugin {
5757
flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) {
5858
self.flush()
5959
}
60+
// Add DestinationMetadata enrichment plugin
61+
add(plugin: DestinationMetadataPlugin())
6062
}
6163

6264
public func update(settings: Settings, type: UpdateType) {
@@ -72,8 +74,7 @@ public class SegmentDestination: DestinationPlugin {
7274
public func execute<T: RawEvent>(event: T?) -> T? {
7375
let result: T? = event
7476
if let r = result {
75-
let modified = configureCloudDestinations(event: r)
76-
queueEvent(event: modified)
77+
queueEvent(event: r)
7778
}
7879
return result
7980
}
@@ -143,37 +144,6 @@ public class SegmentDestination: DestinationPlugin {
143144
}
144145
}
145146

146-
// MARK: - Utility methods
147-
extension SegmentDestination {
148-
internal func configureCloudDestinations<T: RawEvent>(event: T) -> T {
149-
guard let integrationSettings = analytics?.settings() else { return event }
150-
guard let plugins = analytics?.timeline.plugins[.destination]?.plugins as? [DestinationPlugin] else { return event }
151-
guard let customerValues = event.integrations?.dictionaryValue else { return event }
152-
153-
var merged = [String: Any]()
154-
155-
// compare settings to loaded plugins
156-
for plugin in plugins {
157-
let hasSettings = integrationSettings.hasIntegrationSettings(forPlugin: plugin)
158-
if hasSettings {
159-
// we have a device mode plugin installed.
160-
// tell segment not to send it via cloud mode.
161-
merged[plugin.key] = false
162-
}
163-
}
164-
165-
// apply customer values; the customer is always right!
166-
for (key, value) in customerValues {
167-
merged[key] = value
168-
}
169-
170-
var modified = event
171-
modified.integrations = try? JSON(merged)
172-
173-
return modified
174-
}
175-
}
176-
177147
// MARK: - Upload management
178148

179149
extension SegmentDestination {

Sources/Segment/Types.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@
88
import Foundation
99
import Sovran
1010

11+
// MARK: - Supplementary Types
12+
13+
public struct DestinationMetadata: Codable {
14+
var bundled: [String] = []
15+
var unbundled: [String] = []
16+
var bundledIds: [String] = []
17+
}
1118

1219
// MARK: - Event Types
1320

1421
public protocol RawEvent: Codable {
15-
1622
var type: String? { get set }
1723
var anonymousId: String? { get set }
1824
var messageId: String? { get set }
@@ -22,6 +28,7 @@ public protocol RawEvent: Codable {
2228
var context: JSON? { get set }
2329
var integrations: JSON? { get set }
2430
var metrics: [JSON]? { get set }
31+
var _metadata: DestinationMetadata? { get set }
2532
}
2633

2734
public struct TrackEvent: RawEvent {
@@ -33,6 +40,7 @@ public struct TrackEvent: RawEvent {
3340
public var context: JSON? = nil
3441
public var integrations: JSON? = nil
3542
public var metrics: [JSON]? = nil
43+
public var _metadata: DestinationMetadata? = nil
3644

3745
public var event: String
3846
public var properties: JSON?
@@ -57,9 +65,11 @@ public struct IdentifyEvent: RawEvent {
5765
public var context: JSON? = nil
5866
public var integrations: JSON? = nil
5967
public var metrics: [JSON]? = nil
68+
public var _metadata: DestinationMetadata? = nil
6069

6170
public var traits: JSON?
6271

72+
6373
public init(userId: String? = nil, traits: JSON? = nil) {
6474
self.userId = userId
6575
self.traits = traits
@@ -80,6 +90,7 @@ public struct ScreenEvent: RawEvent {
8090
public var context: JSON? = nil
8191
public var integrations: JSON? = nil
8292
public var metrics: [JSON]? = nil
93+
public var _metadata: DestinationMetadata? = nil
8394

8495
public var name: String?
8596
public var category: String?
@@ -106,6 +117,7 @@ public struct GroupEvent: RawEvent {
106117
public var context: JSON? = nil
107118
public var integrations: JSON? = nil
108119
public var metrics: [JSON]? = nil
120+
public var _metadata: DestinationMetadata? = nil
109121

110122
public var groupId: String?
111123
public var traits: JSON?
@@ -129,6 +141,7 @@ public struct AliasEvent: RawEvent {
129141
public var context: JSON? = nil
130142
public var integrations: JSON? = nil
131143
public var metrics: [JSON]? = nil
144+
public var _metadata: DestinationMetadata? = nil
132145

133146
public var userId: String?
134147
public var previousId: String?
@@ -271,6 +284,7 @@ extension RawEvent {
271284
timestamp = e.timestamp
272285
context = e.context
273286
integrations = e.integrations
287+
_metadata = e._metadata
274288
}
275289
}
276290

@@ -284,6 +298,7 @@ extension RawEvent {
284298
result.messageId = UUID().uuidString
285299
result.timestamp = Date().iso8601()
286300
result.integrations = try? JSON([String: Any]())
301+
result._metadata = DestinationMetadata()
287302

288303
return result
289304
}

Sources/Segment/Utilities/HTTPClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public class HTTPClient {
6565
/// - completion: The closure executed when done. Passes if the task should be retried or not if failed.
6666
@discardableResult
6767
func startBatchUpload(writeKey: String, batch: URL, completion: @escaping (_ result: Result<Bool, Error>) -> Void) -> URLSessionDataTask? {
68-
guard let uploadURL = segmentURL(for: apiHost, path: "/batch") else {
68+
guard let uploadURL = segmentURL(for: apiHost, path: "/b") else {
6969
completion(.failure(HTTPClientErrors.failedToOpenBatch))
7070
return nil
7171
}

Sources/Segment/Utilities/Storage.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ extension Storage {
294294
let sentAt = Date().iso8601()
295295

296296
// write it to the existing file
297-
let fileEnding = "],\"sentAt\":\"\(sentAt)\"}"
297+
let fileEnding = "],\"sentAt\":\"\(sentAt)\",\"writeKey\":\"\(writeKey)\"}"
298298
let endData = fileEnding.data(using: .utf8)
299299
if let endData = endData {
300300
fileHandle.seekToEndOfFile()

Tests/Segment-Tests/Analytics_Tests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,51 @@ final class Analytics_Tests: XCTestCase {
340340

341341
XCTAssertEqual(eventVersion, analyticsVersion)
342342
}
343+
344+
class AnyDestination: DestinationPlugin {
345+
var timeline: Timeline
346+
let type: PluginType
347+
let key: String
348+
var analytics: Analytics?
349+
350+
init(key: String) {
351+
self.key = key
352+
self.type = .destination
353+
self.timeline = Timeline()
354+
}
355+
}
356+
357+
func testDestinationMetadata() {
358+
let analytics = Analytics(configuration: Configuration(writeKey: "test"))
359+
let mixpanel = AnyDestination(key: "Mixpanel")
360+
let outputReader = OutputReaderPlugin()
361+
analytics.add(plugin: outputReader)
362+
analytics.add(plugin: mixpanel)
363+
analytics.add(plugin: DestinationMetadataPlugin())
364+
var settings = Settings(writeKey: "123")
365+
let integrations = try? JSON([
366+
"Segment.io": JSON([
367+
"unbundledIntegrations":
368+
[
369+
"Customer.io",
370+
"Mixpanel",
371+
"Amplitude"
372+
]
373+
]),
374+
"Mixpanel": JSON(["someKey": "someVal"])
375+
])
376+
settings.integrations = integrations
377+
analytics.store.dispatch(action: System.UpdateSettingsAction(settings: settings))
378+
379+
waitUntilStarted(analytics: analytics)
380+
381+
382+
analytics.track(name: "sampleEvent")
383+
384+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
385+
let metadata = trackEvent?._metadata
386+
387+
XCTAssertEqual(metadata?.bundled, ["Mixpanel"])
388+
XCTAssertEqual(metadata?.unbundled, ["Customer.io", "Amplitude"])
389+
}
343390
}

0 commit comments

Comments
 (0)