Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding optional gzip compression for /track #653

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading