Skip to content

Commit a8dd5ff

Browse files
Adding optional gzip compression for /track (#653)
* Adding optional gzip compression for /track * Logger->MixpanelLogger * safe unwrap in gzipCompressed --------- Co-authored-by: Jared McFarland <[email protected]>
1 parent 773dd92 commit a8dd5ff

File tree

9 files changed

+190
-27
lines changed

9 files changed

+190
-27
lines changed

Mixpanel-swift.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Pod::Spec.new do |s|
2121
'Sources/Constants.swift', 'Sources/MixpanelType.swift', 'Sources/Mixpanel.swift', 'Sources/MixpanelInstance.swift',
2222
'Sources/Flush.swift','Sources/Track.swift', 'Sources/People.swift', 'Sources/AutomaticEvents.swift',
2323
'Sources/Group.swift',
24-
'Sources/ReadWriteLock.swift', 'Sources/SessionMetadata.swift', 'Sources/MPDB.swift', 'Sources/MixpanelPersistence.swift']
24+
'Sources/ReadWriteLock.swift', 'Sources/SessionMetadata.swift', 'Sources/MPDB.swift', 'Sources/MixpanelPersistence.swift', 'Sources/Data+Compression.swift']
2525
s.tvos.deployment_target = '11.0'
2626
s.tvos.frameworks = 'UIKit', 'Foundation'
2727
s.tvos.pod_target_xcconfig = {

Mixpanel.xcodeproj/project.pbxproj

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545
86F86EF7224554B900B69832 /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 673ABE3921360CBE00B1784B /* Group.swift */; };
4646
86F86F3622497F1200B69832 /* AutomaticEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */; };
4747
86F86F3722497F2900B69832 /* AutomaticEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */; };
48+
95ECF0682C9B851A006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
49+
95ECF0692C9B851B006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
50+
95ECF06A2C9B851B006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
51+
95ECF06B2C9B851C006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
4852
BB9614171F3BB87700C3EF3E /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9614161F3BB87700C3EF3E /* ReadWriteLock.swift */; };
4953
E10D118D1EC0F30900195CCD /* AutomaticEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */; };
5054
E115948B1CFF1538007F8B4F /* Mixpanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948A1CFF1538007F8B4F /* Mixpanel.swift */; };
@@ -107,6 +111,7 @@
107111
8625BEBA26D045CE0009BAA9 /* MPDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MPDB.swift; sourceTree = "<group>"; };
108112
868550AB2699096F001FCDDC /* MixpanelPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixpanelPersistence.swift; sourceTree = "<group>"; };
109113
86F86E81224404BD00B69832 /* Mixpanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mixpanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
114+
95ECF0662C9B83D8006364D2 /* Data+Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Compression.swift"; sourceTree = "<group>"; };
110115
BB9614161F3BB87700C3EF3E /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = "<group>"; };
111116
E115947D1CFF1491007F8B4F /* Mixpanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mixpanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
112117
E11594821CFF1491007F8B4F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Info.plist; sourceTree = "<group>"; };
@@ -254,6 +259,7 @@
254259
E189D8FA1D5A692A007F3F29 /* Utilities */ = {
255260
isa = PBXGroup;
256261
children = (
262+
95ECF0662C9B83D8006364D2 /* Data+Compression.swift */,
257263
E11594981D01689F007F8B4F /* JSONHandler.swift */,
258264
E1982BFE1D0AC2E2006B7330 /* Error.swift */,
259265
E1D335CF1D3059A800E68E12 /* AutomaticProperties.swift */,
@@ -484,6 +490,7 @@
484490
86F86EC522443A2C00B69832 /* People.swift in Sources */,
485491
86F86EC422443A2300B69832 /* ReadWriteLock.swift in Sources */,
486492
8625BEBE26D045CE0009BAA9 /* MPDB.swift in Sources */,
493+
95ECF06B2C9B851C006364D2 /* Data+Compression.swift in Sources */,
487494
86F86EC222443A1300B69832 /* Track.swift in Sources */,
488495
86F86EC122443A0E00B69832 /* JSONHandler.swift in Sources */,
489496
86F86EC022443A0800B69832 /* MixpanelType.swift in Sources */,
@@ -512,6 +519,7 @@
512519
E11594971D006022007F8B4F /* Network.swift in Sources */,
513520
E15FF7C81D0435670076CDE3 /* People.swift in Sources */,
514521
673ABE3A21360CBE00B1784B /* Group.swift in Sources */,
522+
95ECF0682C9B851A006364D2 /* Data+Compression.swift in Sources */,
515523
E11594A11D01C597007F8B4F /* Track.swift in Sources */,
516524
E11594991D01689F007F8B4F /* JSONHandler.swift in Sources */,
517525
E1D335D01D3059A800E68E12 /* AutomaticProperties.swift in Sources */,
@@ -540,6 +548,7 @@
540548
E12782BF1D4AB5CB0025FB05 /* MixpanelInstance.swift in Sources */,
541549
E12782C11D4AB5CB0025FB05 /* Network.swift in Sources */,
542550
8625BEBC26D045CE0009BAA9 /* MPDB.swift in Sources */,
551+
95ECF0692C9B851B006364D2 /* Data+Compression.swift in Sources */,
543552
E12782C21D4AB5CB0025FB05 /* JSONHandler.swift in Sources */,
544553
E12782C31D4AB5CB0025FB05 /* Flush.swift in Sources */,
545554
E12782C41D4AB5CB0025FB05 /* FlushRequest.swift in Sources */,
@@ -568,6 +577,7 @@
568577
E1F15FD61E64B5FC00391AE3 /* FlushRequest.swift in Sources */,
569578
E1F15FD71E64B60200391AE3 /* PrintLogging.swift in Sources */,
570579
8625BEBD26D045CE0009BAA9 /* MPDB.swift in Sources */,
580+
95ECF06A2C9B851B006364D2 /* Data+Compression.swift in Sources */,
571581
E1F15FE21E64B60D00391AE3 /* Flush.swift in Sources */,
572582
E1F15FD51E64B5F800391AE3 /* Network.swift in Sources */,
573583
E1F15FDE1E64B60A00391AE3 /* MixpanelType.swift in Sources */,
@@ -805,7 +815,7 @@
805815
HEADER_SEARCH_PATHS = "";
806816
INFOPLIST_FILE = Info.plist;
807817
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
808-
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
818+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
809819
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
810820
OTHER_SWIFT_FLAGS = "";
811821
PRODUCT_BUNDLE_IDENTIFIER = com.mixpanel.Mixpanel;
@@ -853,7 +863,7 @@
853863
HEADER_SEARCH_PATHS = "";
854864
INFOPLIST_FILE = Info.plist;
855865
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
856-
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
866+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
857867
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
858868
OTHER_SWIFT_FLAGS = "";
859869
PRODUCT_BUNDLE_IDENTIFIER = com.mixpanel.Mixpanel;

MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,4 +1189,14 @@ class MixpanelDemoTests: MixpanelBaseTests {
11891189
}
11901190
}
11911191
}
1192+
1193+
func testGzipCompressionInit() {
1194+
let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, useGzipCompression: true)
1195+
XCTAssertTrue(testMixpanel.useGzipCompression == true, "the init of GzipCompression failed")
1196+
}
1197+
1198+
func testGzipCompressionDefault() {
1199+
let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false)
1200+
XCTAssertTrue(testMixpanel.useGzipCompression == false, "the default gzip option disabled failed")
1201+
}
11921202
}

Sources/Constants.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ struct BundleConstants {
2727
static let ID = "com.mixpanel.Mixpanel"
2828
}
2929

30+
struct GzipSettings {
31+
static let gzipHeaderOffset = Int32(16)
32+
}
33+
3034
#if !os(OSX) && !os(watchOS) && !os(visionOS)
3135
extension UIDevice {
3236
var iPhoneX: Bool {

Sources/Data+Compression.swift

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// Data+Compression.swift
3+
// MixpanelSessionReplay
4+
//
5+
// Copyright © 2024 Mixpanel. All rights reserved.
6+
//
7+
8+
import Foundation
9+
import zlib
10+
11+
public enum GzipError: Swift.Error {
12+
case stream
13+
case data
14+
case memory
15+
case buffer
16+
case version
17+
case unknown(code: Int)
18+
19+
init(code: Int32) {
20+
switch code {
21+
case Z_STREAM_ERROR:
22+
self = .stream
23+
case Z_DATA_ERROR:
24+
self = .data
25+
case Z_MEM_ERROR:
26+
self = .memory
27+
case Z_BUF_ERROR:
28+
self = .buffer
29+
case Z_VERSION_ERROR:
30+
self = .version
31+
default:
32+
self = .unknown(code: Int(code))
33+
}
34+
}
35+
}
36+
37+
extension Data {
38+
/// Compresses the data using gzip compression.
39+
/// Adapted from: https://github.com/1024jp/GzipSwift/blob/main/Sources/Gzip/Data%2BGzip.swift
40+
/// - Parameter level: Compression level.
41+
/// - Returns: The compressed data.
42+
/// - Throws: `GzipError` if compression fails.
43+
public func gzipCompressed(level: Int32 = Z_DEFAULT_COMPRESSION) throws -> Data {
44+
guard !self.isEmpty else {
45+
MixpanelLogger.warn(message: "Empty Data object cannot be compressed.")
46+
return Data()
47+
}
48+
49+
let originalSize = self.count
50+
51+
var stream = z_stream()
52+
stream.next_in = UnsafeMutablePointer<Bytef>(mutating: (self as NSData).bytes.bindMemory(to: Bytef.self, capacity: self.count))
53+
stream.avail_in = uint(self.count)
54+
55+
let windowBits = MAX_WBITS + GzipSettings.gzipHeaderOffset // Use gzip header instead of zlib header
56+
let memLevel = MAX_MEM_LEVEL
57+
let strategy = Z_DEFAULT_STRATEGY
58+
59+
var status = deflateInit2_(&stream, level, Z_DEFLATED, windowBits, memLevel, strategy, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size))
60+
guard status == Z_OK else {
61+
throw GzipError(code: status)
62+
}
63+
64+
var compressedData = Data(count: self.count / 2)
65+
repeat {
66+
if Int(stream.total_out) >= compressedData.count {
67+
compressedData.count += self.count / 2
68+
}
69+
let bufferPointer = compressedData.withUnsafeMutableBytes { $0.baseAddress?.assumingMemoryBound(to: Bytef.self) }
70+
guard let bufferPointer = bufferPointer else {
71+
throw GzipError(code: Z_BUF_ERROR)
72+
}
73+
stream.next_out = bufferPointer.advanced(by: Int(stream.total_out))
74+
stream.avail_out = uint(compressedData.count) - uint(stream.total_out)
75+
76+
status = deflate(&stream, Z_FINISH)
77+
} while stream.avail_out == 0 && status == Z_OK
78+
79+
guard status == Z_STREAM_END else {
80+
throw GzipError(code: status)
81+
}
82+
83+
deflateEnd(&stream)
84+
compressedData.count = Int(stream.total_out)
85+
86+
let compressedSize = compressedData.count
87+
let compressionRatio = Double(compressedSize) / Double(originalSize)
88+
let compressionPercentage = (1 - compressionRatio) * 100
89+
90+
let roundedCompressionRatio = floor(compressionRatio * 1000) / 1000
91+
let roundedCompressionPercentage = floor(compressionPercentage * 1000) / 1000
92+
93+
MixpanelLogger.info(message: "Payload gzipped: original size = \(originalSize) bytes, compressed size = \(compressedSize) bytes, compression ratio = \(roundedCompressionRatio), compression percentage = \(roundedCompressionPercentage)%")
94+
95+
return compressedData
96+
}
97+
}

Sources/Flush.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Flush: AppLifecycle {
2828
private var _serverURL = BasePath.DefaultMixpanelAPI
2929
private let flushRequestReadWriteLock: DispatchQueue
3030

31+
var useGzipCompression: Bool
3132

3233
var serverURL: String {
3334
get {
@@ -68,8 +69,9 @@ class Flush: AppLifecycle {
6869
}
6970
}
7071

71-
required init(serverURL: String) {
72+
required init(serverURL: String, useGzipCompression: Bool) {
7273
self.flushRequest = FlushRequest(serverURL: serverURL)
74+
self.useGzipCompression = useGzipCompression
7375
_serverURL = serverURL
7476
flushRequestReadWriteLock = DispatchQueue(label: "com.mixpanel.flush_interval.lock", qos: .utility, attributes: .concurrent, autoreleaseFrequency: .workItem)
7577
}
@@ -135,7 +137,7 @@ class Flush: AppLifecycle {
135137
type: type,
136138
useIP: useIPAddressForGeoLocation,
137139
headers: headers,
138-
queryItems: queryItems)
140+
queryItems: queryItems, useGzipCompression: useGzipCompression)
139141
#if os(iOS)
140142
if !MixpanelInstance.isiOSAppExtension() {
141143
delegate?.updateNetworkActivityIndicator(false)

Sources/FlushRequest.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ class FlushRequest: Network {
2323
type: FlushType,
2424
useIP: Bool,
2525
headers: [String: String],
26-
queryItems: [URLQueryItem] = []) -> Bool {
26+
queryItems: [URLQueryItem] = [],
27+
useGzipCompression: Bool) -> Bool {
2728

2829
let responseParser: (Data) -> Int? = { data in
2930
let response = String(data: data, encoding: String.Encoding.utf8)
@@ -33,14 +34,25 @@ class FlushRequest: Network {
3334
return nil
3435
}
3536

36-
let resourceHeaders: [String: String] = ["Content-Type": "application/json"].merging(headers) {(_,new) in new }
37-
37+
var resourceHeaders: [String: String] = ["Content-Type": "application/json"].merging(headers) {(_,new) in new }
38+
var compressedData: Data? = nil
39+
40+
if useGzipCompression && type == .events {
41+
if let requestDataRaw = requestData.data(using: .utf8) {
42+
do {
43+
compressedData = try requestDataRaw.gzipCompressed()
44+
resourceHeaders["Content-Encoding"] = "gzip"
45+
} catch {
46+
MixpanelLogger.error(message: "Failed to compress data with gzip: \(error)")
47+
}
48+
}
49+
}
3850
let ipString = useIP ? "1" : "0"
3951
var resourceQueryItems: [URLQueryItem] = [URLQueryItem(name: "ip", value: ipString)]
4052
resourceQueryItems.append(contentsOf: queryItems)
4153
let resource = Network.buildResource(path: type.rawValue,
4254
method: .post,
43-
requestBody: requestData.data(using: .utf8),
55+
requestBody: compressedData ?? requestData.data(using: .utf8),
4456
queryItems: resourceQueryItems,
4557
headers: resourceHeaders,
4658
parse: responseParser)

0 commit comments

Comments
 (0)