Skip to content

Commit

Permalink
Adding optional gzip compression for /track
Browse files Browse the repository at this point in the history
  • Loading branch information
santigracia committed Sep 18, 2024
1 parent 61ce9b4 commit ee0a118
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 27 deletions.
2 changes: 1 addition & 1 deletion Mixpanel-swift.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Pod::Spec.new do |s|
'Sources/Constants.swift', 'Sources/MixpanelType.swift', 'Sources/Mixpanel.swift', 'Sources/MixpanelInstance.swift',
'Sources/Flush.swift','Sources/Track.swift', 'Sources/People.swift', 'Sources/AutomaticEvents.swift',
'Sources/Group.swift',
'Sources/ReadWriteLock.swift', 'Sources/SessionMetadata.swift', 'Sources/MPDB.swift', 'Sources/MixpanelPersistence.swift']
'Sources/ReadWriteLock.swift', 'Sources/SessionMetadata.swift', 'Sources/MPDB.swift', 'Sources/MixpanelPersistence.swift', 'Sources/Data+Compression.swift']
s.tvos.deployment_target = '11.0'
s.tvos.frameworks = 'UIKit', 'Foundation'
s.tvos.pod_target_xcconfig = {
Expand Down
14 changes: 12 additions & 2 deletions Mixpanel.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
86F86EF7224554B900B69832 /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 673ABE3921360CBE00B1784B /* Group.swift */; };
86F86F3622497F1200B69832 /* AutomaticEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */; };
86F86F3722497F2900B69832 /* AutomaticEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */; };
95ECF0682C9B851A006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
95ECF0692C9B851B006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
95ECF06A2C9B851B006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
95ECF06B2C9B851C006364D2 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ECF0662C9B83D8006364D2 /* Data+Compression.swift */; };
BB9614171F3BB87700C3EF3E /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9614161F3BB87700C3EF3E /* ReadWriteLock.swift */; };
E10D118D1EC0F30900195CCD /* AutomaticEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */; };
E115948B1CFF1538007F8B4F /* Mixpanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948A1CFF1538007F8B4F /* Mixpanel.swift */; };
Expand Down Expand Up @@ -107,6 +111,7 @@
8625BEBA26D045CE0009BAA9 /* MPDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MPDB.swift; sourceTree = "<group>"; };
868550AB2699096F001FCDDC /* MixpanelPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixpanelPersistence.swift; sourceTree = "<group>"; };
86F86E81224404BD00B69832 /* Mixpanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mixpanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
95ECF0662C9B83D8006364D2 /* Data+Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Compression.swift"; sourceTree = "<group>"; };
BB9614161F3BB87700C3EF3E /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = "<group>"; };
E115947D1CFF1491007F8B4F /* Mixpanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mixpanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E11594821CFF1491007F8B4F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -254,6 +259,7 @@
E189D8FA1D5A692A007F3F29 /* Utilities */ = {
isa = PBXGroup;
children = (
95ECF0662C9B83D8006364D2 /* Data+Compression.swift */,
E11594981D01689F007F8B4F /* JSONHandler.swift */,
E1982BFE1D0AC2E2006B7330 /* Error.swift */,
E1D335CF1D3059A800E68E12 /* AutomaticProperties.swift */,
Expand Down Expand Up @@ -484,6 +490,7 @@
86F86EC522443A2C00B69832 /* People.swift in Sources */,
86F86EC422443A2300B69832 /* ReadWriteLock.swift in Sources */,
8625BEBE26D045CE0009BAA9 /* MPDB.swift in Sources */,
95ECF06B2C9B851C006364D2 /* Data+Compression.swift in Sources */,
86F86EC222443A1300B69832 /* Track.swift in Sources */,
86F86EC122443A0E00B69832 /* JSONHandler.swift in Sources */,
86F86EC022443A0800B69832 /* MixpanelType.swift in Sources */,
Expand Down Expand Up @@ -512,6 +519,7 @@
E11594971D006022007F8B4F /* Network.swift in Sources */,
E15FF7C81D0435670076CDE3 /* People.swift in Sources */,
673ABE3A21360CBE00B1784B /* Group.swift in Sources */,
95ECF0682C9B851A006364D2 /* Data+Compression.swift in Sources */,
E11594A11D01C597007F8B4F /* Track.swift in Sources */,
E11594991D01689F007F8B4F /* JSONHandler.swift in Sources */,
E1D335D01D3059A800E68E12 /* AutomaticProperties.swift in Sources */,
Expand Down Expand Up @@ -540,6 +548,7 @@
E12782BF1D4AB5CB0025FB05 /* MixpanelInstance.swift in Sources */,
E12782C11D4AB5CB0025FB05 /* Network.swift in Sources */,
8625BEBC26D045CE0009BAA9 /* MPDB.swift in Sources */,
95ECF0692C9B851B006364D2 /* Data+Compression.swift in Sources */,
E12782C21D4AB5CB0025FB05 /* JSONHandler.swift in Sources */,
E12782C31D4AB5CB0025FB05 /* Flush.swift in Sources */,
E12782C41D4AB5CB0025FB05 /* FlushRequest.swift in Sources */,
Expand Down Expand Up @@ -568,6 +577,7 @@
E1F15FD61E64B5FC00391AE3 /* FlushRequest.swift in Sources */,
E1F15FD71E64B60200391AE3 /* PrintLogging.swift in Sources */,
8625BEBD26D045CE0009BAA9 /* MPDB.swift in Sources */,
95ECF06A2C9B851B006364D2 /* Data+Compression.swift in Sources */,
E1F15FE21E64B60D00391AE3 /* Flush.swift in Sources */,
E1F15FD51E64B5F800391AE3 /* Network.swift in Sources */,
E1F15FDE1E64B60A00391AE3 /* MixpanelType.swift in Sources */,
Expand Down Expand Up @@ -805,7 +815,7 @@
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.mixpanel.Mixpanel;
Expand Down Expand Up @@ -853,7 +863,7 @@
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.mixpanel.Mixpanel;
Expand Down
10 changes: 10 additions & 0 deletions MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1189,4 +1189,14 @@ class MixpanelDemoTests: MixpanelBaseTests {
}
}
}

func testGzipCompressionInit() {
let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, useGzipCompression: true)
XCTAssertTrue(testMixpanel.useGzipCompression == true, "the init of GzipCompression failed")
}

func testGzipCompressionDefault() {
let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false)
XCTAssertTrue(testMixpanel.useGzipCompression == false, "the default gzip option disabled failed")
}
}
4 changes: 4 additions & 0 deletions Sources/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ struct BundleConstants {
static let ID = "com.mixpanel.Mixpanel"
}

struct GzipSettings {
static let gzipHeaderOffset = Int32(16)
}

#if !os(OSX) && !os(watchOS) && !os(visionOS)
extension UIDevice {
var iPhoneX: Bool {
Expand Down
93 changes: 93 additions & 0 deletions Sources/Data+Compression.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// Data+Compression.swift
// MixpanelSessionReplay
//
// Copyright © 2024 Mixpanel. All rights reserved.
//

import Foundation
import zlib

public enum GzipError: Swift.Error {
case stream
case data
case memory
case buffer
case version
case unknown(code: Int)

init(code: Int32) {
switch code {
case Z_STREAM_ERROR:
self = .stream
case Z_DATA_ERROR:
self = .data
case Z_MEM_ERROR:
self = .memory
case Z_BUF_ERROR:
self = .buffer
case Z_VERSION_ERROR:
self = .version
default:
self = .unknown(code: Int(code))
}
}
}

extension Data {
/// Compresses the data using gzip compression.
/// Adapted from: https://github.com/1024jp/GzipSwift/blob/main/Sources/Gzip/Data%2BGzip.swift
/// - Parameter level: Compression level.
/// - Returns: The compressed data.
/// - Throws: `GzipError` if compression fails.
public func gzipCompressed(level: Int32 = Z_DEFAULT_COMPRESSION) throws -> Data {
guard !self.isEmpty else {
Logger.warn(message: "Empty Data object cannot be compressed.")
return Data()
}

let originalSize = self.count

var stream = z_stream()
stream.next_in = UnsafeMutablePointer<Bytef>(mutating: (self as NSData).bytes.bindMemory(to: Bytef.self, capacity: self.count))
stream.avail_in = uint(self.count)

let windowBits = MAX_WBITS + GzipSettings.gzipHeaderOffset // Use gzip header instead of zlib header
let memLevel = MAX_MEM_LEVEL
let strategy = Z_DEFAULT_STRATEGY

var status = deflateInit2_(&stream, level, Z_DEFLATED, windowBits, memLevel, strategy, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size))
guard status == Z_OK else {
throw GzipError(code: status)
}

var compressedData = Data(count: self.count / 2)
repeat {
if Int(stream.total_out) >= compressedData.count {
compressedData.count += self.count / 2
}
stream.next_out = compressedData.withUnsafeMutableBytes { $0.baseAddress!.assumingMemoryBound(to: Bytef.self) }.advanced(by: Int(stream.total_out))
stream.avail_out = uint(compressedData.count) - uint(stream.total_out)

status = deflate(&stream, Z_FINISH)
} while stream.avail_out == 0 && status == Z_OK

guard status == Z_STREAM_END else {
throw GzipError(code: status)
}

deflateEnd(&stream)
compressedData.count = Int(stream.total_out)

let compressedSize = compressedData.count
let compressionRatio = Double(compressedSize) / Double(originalSize)
let compressionPercentage = (1 - compressionRatio) * 100

let roundedCompressionRatio = floor(compressionRatio * 1000) / 1000
let roundedCompressionPercentage = floor(compressionPercentage * 1000) / 1000

Logger.info(message: "Payload gzipped: original size = \(originalSize) bytes, compressed size = \(compressedSize) bytes, compression ratio = \(roundedCompressionRatio), compression percentage = \(roundedCompressionPercentage)%")

return compressedData
}
}
6 changes: 4 additions & 2 deletions Sources/Flush.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Flush: AppLifecycle {
private var _serverURL = BasePath.DefaultMixpanelAPI
private let flushRequestReadWriteLock: DispatchQueue

var useGzipCompression: Bool

var serverURL: String {
get {
Expand Down Expand Up @@ -68,8 +69,9 @@ class Flush: AppLifecycle {
}
}

required init(serverURL: String) {
required init(serverURL: String, useGzipCompression: Bool) {
self.flushRequest = FlushRequest(serverURL: serverURL)
self.useGzipCompression = useGzipCompression
_serverURL = serverURL
flushRequestReadWriteLock = DispatchQueue(label: "com.mixpanel.flush_interval.lock", qos: .utility, attributes: .concurrent, autoreleaseFrequency: .workItem)
}
Expand Down Expand Up @@ -135,7 +137,7 @@ class Flush: AppLifecycle {
type: type,
useIP: useIPAddressForGeoLocation,
headers: headers,
queryItems: queryItems)
queryItems: queryItems, useGzipCompression: useGzipCompression)
#if os(iOS)
if !MixpanelInstance.isiOSAppExtension() {
delegate?.updateNetworkActivityIndicator(false)
Expand Down
20 changes: 16 additions & 4 deletions Sources/FlushRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class FlushRequest: Network {
type: FlushType,
useIP: Bool,
headers: [String: String],
queryItems: [URLQueryItem] = []) -> Bool {
queryItems: [URLQueryItem] = [],
useGzipCompression: Bool) -> Bool {

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

let resourceHeaders: [String: String] = ["Content-Type": "application/json"].merging(headers) {(_,new) in new }

var resourceHeaders: [String: String] = ["Content-Type": "application/json"].merging(headers) {(_,new) in new }
var compressedData: Data? = nil

if useGzipCompression && type == .events {
if let requestDataRaw = requestData.data(using: .utf8) {
do {
compressedData = try requestDataRaw.gzipCompressed()
resourceHeaders["Content-Encoding"] = "gzip"
} catch {
Logger.error(message: "Failed to compress data with gzip: \(error)")
}
}
}
let ipString = useIP ? "1" : "0"
var resourceQueryItems: [URLQueryItem] = [URLQueryItem(name: "ip", value: ipString)]
resourceQueryItems.append(contentsOf: queryItems)
let resource = Network.buildResource(path: type.rawValue,
method: .post,
requestBody: requestData.data(using: .utf8),
requestBody: compressedData ?? requestData.data(using: .utf8),
queryItems: resourceQueryItems,
headers: resourceHeaders,
parse: responseParser)
Expand Down
Loading

0 comments on commit ee0a118

Please sign in to comment.