move ios to separate repo (#1655)
* change(ios): podspec tagging * change(ios): podspec tagging * change(ios): podspec tagging * change(ios): remove ios source to separate repo * fix(ui): links to ios
This commit is contained in:
parent
34a7a078be
commit
12c99bb7d7
35 changed files with 18 additions and 3104 deletions
|
|
@ -68,7 +68,7 @@ function IdentifyUsersTab(props: Props) {
|
|||
{platform.value === 'web' ? (
|
||||
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
|
||||
) : (
|
||||
<HighlightCode className="swift" text={`ORTracker.shared.setUserID('john@doe.com');`} />
|
||||
<HighlightCode className="swift" text={`OpenReplay.shared.setUserID('john@doe.com');`} />
|
||||
)}
|
||||
{platform.value === 'web' ? (
|
||||
<div className="flex items-center my-2">
|
||||
|
|
@ -119,7 +119,7 @@ function IdentifyUsersTab(props: Props) {
|
|||
) : (
|
||||
<HighlightCode
|
||||
className="swift"
|
||||
text={`ORTracker.shared.setMetadata('plan', 'premium');`}
|
||||
text={`OpenReplay.shared.setMetadata('plan', 'premium');`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,18 @@ import Highlight from 'react-highlight';
|
|||
import CircleNumber from '../../CircleNumber';
|
||||
import { CopyButton } from 'UI';
|
||||
|
||||
const installationCommand = 'add command after publishing!';
|
||||
const installationCommand = `
|
||||
// Cocoapods
|
||||
pod 'Openreplay', '~> 1.0.5'
|
||||
|
||||
// Swift Package Manager
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/openreplay/ios-tracker.git", from: "1.0.5"),
|
||||
]
|
||||
`;
|
||||
|
||||
const usageCode = `// AppDelegate.swift
|
||||
import ORTracker
|
||||
import OpenReplay
|
||||
|
||||
//...
|
||||
|
||||
|
|
@ -15,8 +24,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
ORTracker.shared.serverURL = "https://your.instance.com/ingest"
|
||||
ORTracker.shared.start(projectKey: "PROJECT_KEY", options: .defaults)
|
||||
OpenReplay.shared.serverURL = "https://your.instance.com/ingest"
|
||||
OpenReplay.shared.start(projectKey: "PROJECT_KEY", options: .defaults)
|
||||
|
||||
// ...
|
||||
return true
|
||||
|
|
@ -30,7 +39,7 @@ let screen: Bool
|
|||
let wifiOnly: Bool`;
|
||||
|
||||
const touches = `// SceneDelegate.Swift
|
||||
import ORTracker
|
||||
import OpenReplay
|
||||
|
||||
// ...
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
|
|
@ -46,14 +55,14 @@ import ORTracker
|
|||
}
|
||||
// ...`
|
||||
|
||||
const sensitive = `import ORTracker
|
||||
const sensitive = `import OpenReplay
|
||||
|
||||
// swiftUI
|
||||
Text("Very important sensitive text")
|
||||
.sensitive()
|
||||
|
||||
// UIKit
|
||||
ORTracker.shared.addIgnoredView(view)`
|
||||
OpenReplay.shared.addIgnoredView(view)`
|
||||
|
||||
const inputs = `// swiftUI
|
||||
TextField("Input", text: $text)
|
||||
|
|
|
|||
3
tracker/tracker-ios/.gitignore
vendored
3
tracker/tracker-ios/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
.build
|
||||
.swiftpm
|
||||
.DS_Store
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
Elastic License 2.0 (ELv2)
|
||||
|
||||
**Acceptance**
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
**Copyright License**
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below
|
||||
|
||||
**Limitations**
|
||||
You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
|
||||
|
||||
You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
|
||||
|
||||
You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
**Patents**
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
||||
|
||||
**Notices**
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
|
||||
|
||||
If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
|
||||
|
||||
**No Other Rights**
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
**Termination**
|
||||
If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
|
||||
|
||||
**No Liability**
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
|
||||
|
||||
**Definitions**
|
||||
The *licensor* is the entity offering these terms, and the *software* is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
*you* refers to the individual or entity agreeing to these terms.
|
||||
|
||||
*your company* is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. *control* means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
*your licenses* are all the licenses granted to you for the software under these terms.
|
||||
|
||||
*use* means anything you do with the software requiring one of your licenses.
|
||||
|
||||
*trademark* means trademarks, service marks, and similar rights.
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'ORTracker'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'A short description of ORTracker.'
|
||||
s.homepage = 'https://github.com/openreplay/openreplay/tracker/tracker-ios'
|
||||
s.license = { :type => 'ELv2', :file => 'LICENSE.md' }
|
||||
s.author = { 'Nick Delirium' => 'nick.delirium@proton.me' }
|
||||
s.source = { :git => 'https://github.com/openreplay/openreplay/tracker/tracker-ios', :tag => s.version.to_s }
|
||||
s.ios.deployment_target = '13.0'
|
||||
s.swift_version = '5.0'
|
||||
s.source_files = 'Sources/ORTracker/**/*'
|
||||
s.dependency 'SWCompression'
|
||||
s.dependency 'DeviceKit'
|
||||
end
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "bitbytedata",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tsolomko/BitByteData",
|
||||
"state" : {
|
||||
"revision" : "36df26fe4586b4f23d76cfd8b47076998343a2b2",
|
||||
"version" : "2.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "devicekit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/devicekit/DeviceKit.git",
|
||||
"state" : {
|
||||
"revision" : "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6",
|
||||
"version" : "4.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swcompression",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tsolomko/SWCompression.git",
|
||||
"state" : {
|
||||
"revision" : "cd39ca0a3b269173bab06f68b182b72fa690765c",
|
||||
"version" : "4.8.5"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// swift-tools-version: 5.8
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ORTracker",
|
||||
platforms: [
|
||||
.iOS(.v13)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "ORTracker",
|
||||
targets: ["ORTracker"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/devicekit/DeviceKit.git", from: "4.0.0"),
|
||||
.package(url: "https://github.com/tsolomko/SWCompression.git", .upToNextMajor(from: "4.8.5")),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "ORTracker",
|
||||
dependencies: [
|
||||
.product(name: "SWCompression", package: "SWCompression"),
|
||||
.product(name: "DeviceKit", package: "DeviceKit"),
|
||||
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ORTrackerTests",
|
||||
dependencies: ["ORTracker"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
platform :ios, '13.0'
|
||||
|
||||
target 'ORTracker' do
|
||||
use_frameworks!
|
||||
|
||||
pod 'SWCompression', '~> 4.8'
|
||||
pod 'DeviceKit', '~> 5.1'
|
||||
end
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
Setting up tracker
|
||||
|
||||
|
||||
```swift
|
||||
// AppDelegate.swift
|
||||
import ORTracker
|
||||
|
||||
//...
|
||||
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
ORTracker.shared.serverURL = "https://your.instance.com/ingest"
|
||||
ORTracker.shared.start(projectKey: "projectkey", options: .defaults)
|
||||
|
||||
// ...
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
Options (default all `true`)
|
||||
|
||||
```swift
|
||||
let crashes: Bool
|
||||
let analytics: Bool
|
||||
let performances: Bool
|
||||
let logs: Bool
|
||||
let screen: Bool
|
||||
let wifiOnly: Bool
|
||||
```
|
||||
|
||||
Setting up touches listener
|
||||
|
||||
```swift
|
||||
// SceneDelegate.Swift
|
||||
import ORTracker
|
||||
|
||||
// ...
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
let contentView = ContentView()
|
||||
.environmentObject(TodoStore())
|
||||
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = TouchTrackingWindow(windowScene: windowScene) // <<<< here
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Adding sensitive views (will be blurred in replay)
|
||||
|
||||
```swift
|
||||
import ORTracker
|
||||
|
||||
// swiftUI
|
||||
Text("Very important sensitive text")
|
||||
.sensitive()
|
||||
|
||||
// UIKit
|
||||
ORTracker.shared.addIgnoredView(view)
|
||||
```
|
||||
|
||||
Adding tracked inputs
|
||||
|
||||
```swift
|
||||
|
||||
// swiftUI
|
||||
TextField("Input", text: $text)
|
||||
.observeInput(text: $text, label: "tracker input #1", masked: Bool)
|
||||
|
||||
// UIKit will use placeholder as label and sender.isSecureTextEntry to mask the input
|
||||
Analytics.shared.addObservedInput(inputEl)
|
||||
```
|
||||
|
||||
Observing views
|
||||
|
||||
```swift
|
||||
// swiftUI
|
||||
TextField("Test")
|
||||
.observeView(title: "Screen title", viewName: "test input name")
|
||||
|
||||
// UIKit
|
||||
Analytics.shared.addObservedView(view: inputEl, title: "Screen title", viewName: "test input name")
|
||||
```
|
||||
|
||||
will send IOSScreenEnter and IOSScreenLeave when view appears/dissapears on/from screen
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
import UIKit
|
||||
import CommonCrypto
|
||||
|
||||
protocol NSObjectCoding: NSCoding, NSObject {}
|
||||
|
||||
extension NSObjectCoding {
|
||||
static func from(data: Data, offset: inout Int) throws -> Self {
|
||||
let valueData = try data.readData(offset: &offset)
|
||||
guard let result = try NSKeyedUnarchiver.unarchivedObject(ofClass: self, from: valueData)
|
||||
else { throw NSError(domain: "ErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error reading NSCoding"]) }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
mutating func appendString(_ string: String) {
|
||||
if let data = string.data(using: .utf8) {
|
||||
append(data)
|
||||
}
|
||||
}
|
||||
|
||||
func sha256() -> String {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
self.withUnsafeBytes {
|
||||
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
|
||||
}
|
||||
return Data(hash).hexEncodedString()
|
||||
}
|
||||
|
||||
func hexEncodedString() -> String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
func subdata(start: Int, length: Int) -> Data? {
|
||||
let start = startIndex.advanced(by: start)
|
||||
let end = start.advanced(by: length)
|
||||
guard start >= 0, end <= count else { return nil }
|
||||
return subdata(in: start..<end)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
func readPrimary<T>(offset: inout Int) throws -> T {
|
||||
if T.self == CGFloat.self {
|
||||
return CGFloat(try readPrimary(offset: &offset) as Double) as! T
|
||||
}
|
||||
if T.self == UInt64.self {
|
||||
return try readUint(offset: &offset) as! T
|
||||
}
|
||||
if T.self == Int64.self {
|
||||
return try readInt(offset: &offset) as! T
|
||||
}
|
||||
if T.self == Bool.self {
|
||||
return try readBoolean(offset: &offset) as! T
|
||||
}
|
||||
let valueSize = MemoryLayout<T>.size
|
||||
guard let data = subdata(start: offset, length: valueSize) else { throw "Error reading primary value" }
|
||||
let result = data.withUnsafeBytes {
|
||||
$0.load(as: T.self)
|
||||
}
|
||||
offset += data.count
|
||||
return result
|
||||
}
|
||||
|
||||
func readData(offset: inout Int) throws -> Data {
|
||||
let length = try readUint(offset: &offset)
|
||||
guard let data = subdata(start: offset, length: Int(length)),
|
||||
length == data.count else { throw "Error reading data" }
|
||||
|
||||
offset += Int(length)
|
||||
return data
|
||||
}
|
||||
|
||||
func readString(offset: inout Int) throws -> String {
|
||||
let data = try readData(offset: &offset)
|
||||
guard let result = String(data: data, encoding: .utf8)
|
||||
else { throw "Error reading string" }
|
||||
return result
|
||||
}
|
||||
|
||||
private func readByte(offset: inout Int) throws -> UInt8 {
|
||||
guard offset < count else { throw "Error reading byte" }
|
||||
let b = self[offset]
|
||||
offset += 1
|
||||
return b
|
||||
}
|
||||
|
||||
private func readUint(offset: inout Int) throws -> UInt64 {
|
||||
var x: UInt64 = 0
|
||||
var s: Int = 0
|
||||
var i: Int = 0
|
||||
while true {
|
||||
let b = try readByte(offset: &offset)
|
||||
if b < 0x80 {
|
||||
if i > 9 || i == 9 && b > 1 {
|
||||
throw "Invalid UInt"
|
||||
}
|
||||
return x | UInt64(b)<<s
|
||||
}
|
||||
x |= UInt64(b&0x7f) << s
|
||||
s += 7
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
private func readInt(offset: inout Int) throws -> Int64 {
|
||||
let ux = try readUint(offset: &offset)
|
||||
var x = Int64(ux >> 1)
|
||||
if ux&1 != 0 {
|
||||
x = ~x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
private func readBoolean(offset: inout Int) throws -> Bool {
|
||||
return try readByte(offset: &offset) == 1
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
init(value: Any?) {
|
||||
self.init()
|
||||
if let value = value {
|
||||
writeValue(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
init(values: Any...) {
|
||||
self.init()
|
||||
values.forEach { writeValue(value: $0) }
|
||||
}
|
||||
|
||||
mutating func writeValues(values: Any...) {
|
||||
values.forEach { writeValue(value: $0) }
|
||||
}
|
||||
|
||||
mutating func writeValue(value: Any) {
|
||||
let oldLength = count
|
||||
switch value {
|
||||
case is NSNull: break
|
||||
case let parsed as Data: writeData(parsed, sizePrefix: true)
|
||||
case let parsed as Int64: writeInt(parsed)
|
||||
case let parsed as UInt64: writeUint(parsed)
|
||||
case let parsed as Int: writePrimary(parsed)
|
||||
case let parsed as UInt8: writePrimary(parsed)
|
||||
case let parsed as UInt16: writePrimary(parsed)
|
||||
case let parsed as UInt32: writePrimary(parsed)
|
||||
case let parsed as Float: writePrimary(parsed)
|
||||
case let parsed as CGFloat: writePrimary(Double(parsed))
|
||||
case let parsed as Double: writePrimary(parsed)
|
||||
case let parsed as Bool: writeBoolean(parsed)
|
||||
case let parsed as UIEdgeInsets:
|
||||
writeValues(values: parsed.top, parsed.bottom, parsed.left, parsed.right)
|
||||
case let parsed as CGRect:
|
||||
writeValues(values: parsed.origin.x, parsed.origin.y, parsed.size.width, parsed.size.height)
|
||||
case let parsed as CGPoint: writeValues(values: parsed.x, parsed.y)
|
||||
case let parsed as CGSize: writeValues(values: parsed.width, parsed.height)
|
||||
case let parsed as UIColor:
|
||||
var red: CGFloat = 0
|
||||
var green: CGFloat = 0
|
||||
var blue: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
parsed.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
writeValues(values: red, green, blue, alpha)
|
||||
case let parsed as String: writeString(parsed)
|
||||
case let parsed as UIFont: writeNSCoding(parsed.fontDescriptor)
|
||||
case let parsed as NSAttributedString: writeNSCoding(parsed)
|
||||
default: break
|
||||
}
|
||||
if CFGetTypeID(value as CFTypeRef) == CGColor.typeID {
|
||||
writeValue(value: UIColor(cgColor: value as! CGColor))
|
||||
}
|
||||
let length = count - oldLength
|
||||
if length == 0 && !(value is NSNull) {
|
||||
print("Nothing was written for \(String(describing: type(of: value))):\(value) ")
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func writePrimary<T>(_ value: T) {
|
||||
writeData(Swift.withUnsafeBytes(of: value) { Data($0) }, sizePrefix: false)
|
||||
}
|
||||
|
||||
private mutating func writeString(_ string: String) {
|
||||
let stringData = string.data(using: .utf8, allowLossyConversion: true) ?? Data()
|
||||
writeData(stringData, sizePrefix: true)
|
||||
}
|
||||
|
||||
private mutating func writeNSCoding(_ coding: NSCoding) {
|
||||
do {
|
||||
let valueData = try NSKeyedArchiver.archivedData(withRootObject: coding, requiringSecureCoding: true)
|
||||
writeData(valueData, sizePrefix: true)
|
||||
} catch {
|
||||
print("Unexpected error: \(error).")
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func writeData(_ data: Data, sizePrefix: Bool) {
|
||||
if sizePrefix {
|
||||
writeValue(value: UInt64(data.count))
|
||||
}
|
||||
append(data)
|
||||
}
|
||||
|
||||
private mutating func writeUint(_ input: UInt64) {
|
||||
var v = input
|
||||
while v >= 0x80 {
|
||||
append(UInt8(v.littleEndian & 0x7F) | 0x80) // v.littleEndian ?
|
||||
v >>= 7
|
||||
}
|
||||
append(UInt8(v))
|
||||
}
|
||||
|
||||
private mutating func writeInt(_ v: Int64) {
|
||||
var uv = UInt64(v) << 1
|
||||
if v < 0 {
|
||||
uv = ~uv
|
||||
}
|
||||
writeUint(uv)
|
||||
}
|
||||
|
||||
private mutating func writeBoolean(_ v: Bool) {
|
||||
append(v ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension Encodable {
|
||||
func toJSONData() -> Data? { try? JSONEncoder().encode(self) }
|
||||
}
|
||||
|
||||
extension String: Error {}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
extension Date {
|
||||
func format(_ format: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US")
|
||||
formatter.dateFormat = format
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
|
||||
static func parse(dateStr: String, format: String) -> Date? {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US")
|
||||
formatter.dateFormat = format
|
||||
return formatter.date(from: dateStr)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
func contains(regex: String) -> Bool {
|
||||
guard let regex = try? NSRegularExpression(pattern: regex) else { return false }
|
||||
let range = NSRange(location: 0, length: self.utf16.count)
|
||||
return regex.firstMatch(in: self, options: [], range: range) != nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
func applyBlurWithRadius(_ blurRadius: CGFloat) -> UIImage? {
|
||||
if (size.width < 1 || size.height < 1) {
|
||||
return nil
|
||||
}
|
||||
guard let inputCGImage = self.cgImage else {
|
||||
return nil
|
||||
}
|
||||
let inputImage = CIImage(cgImage: inputCGImage)
|
||||
let filter = CIFilter(name: "CIGaussianBlur")
|
||||
filter?.setValue(inputImage, forKey: kCIInputImageKey)
|
||||
filter?.setValue(blurRadius, forKey: kCIInputRadiusKey)
|
||||
guard let outputImage = filter?.outputImage else {
|
||||
return nil
|
||||
}
|
||||
let context = CIContext(options: nil)
|
||||
guard let outputCGImage = context.createCGImage(outputImage, from: inputImage.extent) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: outputCGImage)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
private var viewCounter = 0
|
||||
private var shortIds = [String: String]()
|
||||
|
||||
extension UIView: Sanitizable {
|
||||
public var identifier: String {
|
||||
let longId = longIdentifier
|
||||
if let existingId = shortIds[longId] {
|
||||
return existingId
|
||||
}
|
||||
let shortId = "\(viewCounter)"
|
||||
viewCounter += 1
|
||||
shortIds[longId] = shortId
|
||||
return shortId
|
||||
}
|
||||
|
||||
public var longIdentifier: String {
|
||||
return String(describing: type(of: self)) + "-" + Unmanaged.passUnretained(self).toOpaque().debugDescription
|
||||
}
|
||||
|
||||
public var frameInWindow: CGRect? {
|
||||
return self.window == nil ? nil : self.convert(self.bounds, to: self.window)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
import UIKit
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import ObjectiveC
|
||||
|
||||
open class Analytics: NSObject {
|
||||
public static let shared = Analytics()
|
||||
public var enabled = false
|
||||
public var observedInputs: [UITextField] = []
|
||||
public var observedViews: [UIView] = []
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func start() {
|
||||
enabled = true
|
||||
UIViewController.swizzleLifecycleMethods()
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
observedViews.removeAll()
|
||||
observedInputs.removeAll()
|
||||
enabled = false
|
||||
// Unswizzle (reverse the swizzling) if needed in the
|
||||
}
|
||||
|
||||
@objc private func handleTap(gesture: UITapGestureRecognizer) {
|
||||
let location = gesture.location(in: nil)
|
||||
DebugUtils.log("Tap detected at: \(location)")
|
||||
}
|
||||
|
||||
@objc public func addObservedInput(_ element: UITextField) {
|
||||
observedInputs.append(element)
|
||||
element.addTarget(self, action: #selector(textInputFinished), for: .editingDidEnd)
|
||||
}
|
||||
|
||||
@objc public func addObservedView(view: UIView, screenName: String, viewName: String) {
|
||||
view.orScreenName = screenName
|
||||
view.orViewName = viewName
|
||||
observedViews.append(view)
|
||||
}
|
||||
|
||||
@objc public func sendClick(label: String, x: UInt64, y: UInt64) {
|
||||
let message = ORIOSClickEvent(label: label, x: x, y: y)
|
||||
|
||||
if Analytics.shared.enabled {
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func sendSwipe(label: String, x: UInt64, y: UInt64, direction: String) {
|
||||
let message = ORIOSSwipeEvent(label: label, x: x,y: y, direction: direction)
|
||||
|
||||
if Analytics.shared.enabled {
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func textInputFinished(_ sender: UITextField) {
|
||||
#if DEBUG
|
||||
DebugUtils.log(">>>>>Text finish \(sender.text ?? "no_text") \(sender.placeholder ?? "no_placeholder")")
|
||||
#endif
|
||||
var sentText = sender.text
|
||||
if sender.isSecureTextEntry {
|
||||
sentText = "***"
|
||||
}
|
||||
MessageCollector.shared.sendMessage(ORIOSInputEvent(value: sentText ?? "", valueMasked: sender.isSecureTextEntry, label: sender.placeholder ?? ""))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
|
||||
static func swizzleLifecycleMethods() {
|
||||
DebugUtils.log(">>>>> ORTracker: swizzle UIViewController")
|
||||
|
||||
Self.swizzle(original: #selector(viewDidAppear(_:)), swizzled: #selector(swizzledViewDidAppear(_:)))
|
||||
Self.swizzle(original: #selector(viewDidDisappear(_:)), swizzled: #selector(swizzledViewDidDisappear(_:)))
|
||||
}
|
||||
|
||||
static private func swizzle(original: Selector, swizzled: Selector) {
|
||||
if let originalMethod = class_getInstanceMethod(self, original),
|
||||
let swizzledMethod = class_getInstanceMethod(self, swizzled) {
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func swizzledViewDidAppear(_ animated: Bool) {
|
||||
self.swizzledViewDidAppear(animated)
|
||||
|
||||
if let (screenName, viewName) = isViewOrSubviewObservedEnter() {
|
||||
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: true)
|
||||
if Analytics.shared.enabled {
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func swizzledViewDidDisappear(_ animated: Bool) {
|
||||
self.swizzledViewDidDisappear(animated)
|
||||
|
||||
if let (screenName, viewName) = isViewOrSubviewObservedEnter() {
|
||||
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: false)
|
||||
if Analytics.shared.enabled {
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isViewOrSubviewObservedEnter() -> (screenName: String, viewName: String)? {
|
||||
var viewsToCheck: [UIView] = [self.view]
|
||||
while !viewsToCheck.isEmpty {
|
||||
let view = viewsToCheck.removeFirst()
|
||||
if let observed = Analytics.shared.observedViews.first(where: { $0 == view }) {
|
||||
let screenName = observed.orScreenName ?? "Unknown ScreenName"
|
||||
let viewName = observed.orViewName ?? "Unknown View"
|
||||
|
||||
return (screenName, viewName)
|
||||
}
|
||||
viewsToCheck.append(contentsOf: view.subviews)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public class TouchTrackingWindow: UIWindow {
|
||||
var touchStart: CGPoint?
|
||||
|
||||
public override func sendEvent(_ event: UIEvent) {
|
||||
super.sendEvent(event)
|
||||
|
||||
guard let touches = event.allTouches else { return }
|
||||
|
||||
for touch in touches {
|
||||
switch touch.phase {
|
||||
case .began:
|
||||
touchStart = touch.location(in: self)
|
||||
case .ended:
|
||||
let location = touch.location(in: self)
|
||||
let isSwipe = touchStart!.distance(to: location) > 10
|
||||
var event: ORMessage
|
||||
let description = getViewDescription(touch.view) ?? "UIView"
|
||||
if isSwipe {
|
||||
DebugUtils.log("Swipe from \(touchStart ?? CGPoint(x: 0, y: 0)) to \(location)")
|
||||
event = ORIOSSwipeEvent(label: description, x: UInt64(location.x),y: UInt64(location.y), direction: detectSwipeDirection(from: touchStart!, to: location))
|
||||
} else {
|
||||
event = ORIOSClickEvent(label: description, x: UInt64(location.x), y: UInt64(location.y))
|
||||
DebugUtils.log("Touch from \(touchStart ?? CGPoint(x: 0, y: 0)) to \(location)")
|
||||
}
|
||||
touchStart = nil
|
||||
MessageCollector.shared.sendMessage(event)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func getViewDescription(_ view: UIView?) -> String? {
|
||||
guard let view = view else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let textField = view as? UITextField {
|
||||
return "UITextField '\(textField.placeholder ?? "No Placeholder")'"
|
||||
} else if let label = view as? UILabel {
|
||||
return "UILabel '\(label.text ?? "No Text")'"
|
||||
} else if let button = view as? UIButton {
|
||||
return "UIButton '\(button.currentTitle ?? "No Title")'"
|
||||
} else if let textView = view as? UITextView {
|
||||
return "UITextView '\(textView.text ?? "No Text")'"
|
||||
} else {
|
||||
return "\(type(of: view))"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func detectSwipeDirection(from start: CGPoint, to end: CGPoint) -> String {
|
||||
let deltaX = end.x - start.x
|
||||
let deltaY = end.y - start.y
|
||||
|
||||
if abs(deltaX) > abs(deltaY) {
|
||||
if deltaX > 0 {
|
||||
return "right"
|
||||
} else {
|
||||
return "left"
|
||||
}
|
||||
} else if abs(deltaY) > abs(deltaX) {
|
||||
if deltaY > 0 {
|
||||
return "down"
|
||||
} else {
|
||||
return "up"
|
||||
}
|
||||
}
|
||||
|
||||
return "right"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
extension CGPoint {
|
||||
func distance(to point: CGPoint) -> CGFloat {
|
||||
return hypot(point.x - x, point.y - y)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ObservedInputModifier: ViewModifier {
|
||||
@Binding var text: String
|
||||
let label: String?
|
||||
let masked: Bool?
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
.onReceive(text.publisher.collect()) { value in
|
||||
let stringValue = String(value)
|
||||
textInputFinished(value: stringValue, label: label, masked: masked)
|
||||
}
|
||||
}
|
||||
|
||||
private func textInputFinished(value: String, label: String?, masked: Bool?) {
|
||||
guard !value.isEmpty else { return }
|
||||
var sentValue = value
|
||||
if masked ?? false {
|
||||
sentValue = "****"
|
||||
}
|
||||
MessageCollector.shared.sendDebouncedMessage(ORIOSInputEvent(value: sentValue, valueMasked: masked ?? false, label: label ?? ""))
|
||||
}
|
||||
}
|
||||
|
||||
public struct ViewLifecycleModifier: ViewModifier {
|
||||
let screenName: String
|
||||
let viewName: String
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
.onAppear {
|
||||
DebugUtils.log("<><><>view appear \(viewName)")
|
||||
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: true)
|
||||
if Analytics.shared.enabled {
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
}
|
||||
.onDisappear {
|
||||
DebugUtils.log("<><><>disappear view \(viewName)")
|
||||
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: false)
|
||||
if Analytics.shared.enabled {
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public extension View {
|
||||
func observeView(screenName: String, viewName: String) -> some View {
|
||||
self.modifier(ViewLifecycleModifier(screenName: screenName, viewName: viewName))
|
||||
}
|
||||
|
||||
func observeInput(text: Binding<String>, label: String?, masked: Bool?) -> some View {
|
||||
self.modifier(ObservedInputModifier(text: text, label: label, masked: masked))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
private struct AssociatedKeys {
|
||||
static var orScreenName: String = "OR: screenName"
|
||||
static var orViewName: String = "OR: viewName"
|
||||
}
|
||||
|
||||
var orScreenName: String? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.orScreenName) as? String
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.orScreenName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
var orViewName: String? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.orViewName) as? String
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.orViewName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
public class Crashs: NSObject {
|
||||
public static let shared = Crashs()
|
||||
private static var fileUrl: URL? = nil
|
||||
private var isActive = false
|
||||
|
||||
private override init() {
|
||||
Crashs.fileUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("ASCrash.dat")
|
||||
if let fileUrl = Crashs.fileUrl,
|
||||
FileManager.default.fileExists(atPath: fileUrl.path),
|
||||
let crashData = try? Data(contentsOf: fileUrl) {
|
||||
NetworkManager.shared.sendLateMessage(content: crashData) { (success) in
|
||||
guard success else { return }
|
||||
if FileManager.default.fileExists(atPath: fileUrl.path) {
|
||||
try? FileManager.default.removeItem(at: fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func start() {
|
||||
NSSetUncaughtExceptionHandler { (exception) in
|
||||
print("<><> captured crash \(exception)")
|
||||
let message = ORIOSCrash(name: exception.name.rawValue,
|
||||
reason: exception.reason ?? "",
|
||||
stacktrace: exception.callStackSymbols.joined(separator: "\n"))
|
||||
let messageData = message.contentData()
|
||||
if let fileUrl = Crashs.fileUrl {
|
||||
try? messageData.write(to: fileUrl)
|
||||
}
|
||||
NetworkManager.shared.sendMessage(content: messageData) { (success) in
|
||||
guard success else { return }
|
||||
if let fileUrl = Crashs.fileUrl,
|
||||
FileManager.default.fileExists(atPath: fileUrl.path) {
|
||||
try? FileManager.default.removeItem(at: fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
isActive = true
|
||||
}
|
||||
|
||||
public func sendLateError(exception: NSException) {
|
||||
let message = ORIOSCrash(name: exception.name.rawValue,
|
||||
reason: exception.reason ?? "",
|
||||
stacktrace: exception.callStackSymbols.joined(separator: "\n")
|
||||
)
|
||||
NetworkManager.shared.sendLateMessage(content: message.contentData()) { (success) in
|
||||
guard success else { return }
|
||||
if let fileUrl = Crashs.fileUrl,
|
||||
FileManager.default.fileExists(atPath: fileUrl.path) {
|
||||
try? FileManager.default.removeItem(at: fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if isActive {
|
||||
NSSetUncaughtExceptionHandler(nil)
|
||||
isActive = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
class LogsListener: NSObject {
|
||||
static let shared = LogsListener()
|
||||
private let outputListener = Listener(fileHandle: FileHandle.standardOutput, severity: "info")
|
||||
private let errorListener = Listener(fileHandle: FileHandle.standardError, severity: "error")
|
||||
|
||||
func start() {
|
||||
outputListener.start()
|
||||
errorListener.start()
|
||||
}
|
||||
|
||||
class Listener: NSObject {
|
||||
let inputPipe = Pipe()
|
||||
let outputPipe = Pipe()
|
||||
let fileHandle: FileHandle
|
||||
let severity: String
|
||||
|
||||
init(fileHandle: FileHandle, severity: String) {
|
||||
self.fileHandle = fileHandle
|
||||
self.severity = severity
|
||||
super.init()
|
||||
|
||||
inputPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
let data = fileHandle.availableData
|
||||
if let string = String(data: data, encoding: String.Encoding.utf8) {
|
||||
let message = ORIOSLog(severity: severity, content: string)
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
strongSelf.outputPipe.fileHandleForWriting.write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func start() {
|
||||
dup2(fileHandle.fileDescriptor, outputPipe.fileHandleForWriting.fileDescriptor)
|
||||
dup2(inputPipe.fileHandleForWriting.fileDescriptor, fileHandle.fileDescriptor)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
open class NetworkListener: NSObject {
|
||||
private let startTime: UInt64
|
||||
private var url: String = ""
|
||||
private var method: String = ""
|
||||
private var requestBody: String?
|
||||
private var requestHeaders: [String: String]?
|
||||
var ignoredKeys = ["password"]
|
||||
var ignoredHeaders = ["Authentication", "Auth"]
|
||||
|
||||
public override init() {
|
||||
startTime = UInt64(Date().timeIntervalSince1970 * 1000)
|
||||
}
|
||||
|
||||
public convenience init(request: URLRequest) {
|
||||
self.init()
|
||||
start(request: request)
|
||||
}
|
||||
|
||||
public convenience init(task: URLSessionTask) {
|
||||
self.init()
|
||||
start(task: task)
|
||||
}
|
||||
|
||||
open func start(request: URLRequest) {
|
||||
url = request.url?.absoluteString ?? ""
|
||||
method = request.httpMethod ?? "GET"
|
||||
requestHeaders = request.allHTTPHeaderFields
|
||||
|
||||
if let body = request.httpBody {
|
||||
requestBody = String(data: body, encoding: .utf8)
|
||||
} else {
|
||||
requestBody = ""
|
||||
DebugUtils.log("error getting request body (start request)")
|
||||
}
|
||||
}
|
||||
|
||||
open func start(task: URLSessionTask) {
|
||||
if let request = task.currentRequest {
|
||||
start(request: request)
|
||||
} else {
|
||||
DebugUtils.log("error getting request body (start task)")
|
||||
}
|
||||
}
|
||||
|
||||
open func finish(response: URLResponse?, data: Data?) {
|
||||
let endTime = UInt64(Date().timeIntervalSince1970 * 1000)
|
||||
let httpResponse = response as? HTTPURLResponse
|
||||
|
||||
var responseBody: String? = nil
|
||||
if let data = data {
|
||||
responseBody = String(data: data, encoding: .utf8)
|
||||
} else {
|
||||
DebugUtils.log("error getting request body (finish)")
|
||||
}
|
||||
|
||||
let requestContent: [String: Any?] = [
|
||||
"body": sanitizeBody(body: requestBody),
|
||||
"headers": sanitizeHeaders(headers: requestHeaders)
|
||||
]
|
||||
|
||||
var responseContent: [String: Any?]
|
||||
if let httpResponse = httpResponse {
|
||||
let headers = transformHeaders(httpResponse.allHeaderFields)
|
||||
responseContent = [
|
||||
"body": sanitizeBody(body: responseBody),
|
||||
"headers": sanitizeHeaders(headers: headers)
|
||||
]
|
||||
} else {
|
||||
responseContent = [
|
||||
"body": "",
|
||||
"headers": ""
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
let requestJSON = convertDictionaryToJSONString(dictionary: requestContent) ?? ""
|
||||
let responseJSON = convertDictionaryToJSONString(dictionary: responseContent) ?? ""
|
||||
|
||||
let status = httpResponse?.statusCode ?? 0
|
||||
let message = ORIOSNetworkCall(
|
||||
type: "request",
|
||||
method: method,
|
||||
URL: url,
|
||||
request: requestJSON,
|
||||
response: responseJSON,
|
||||
status: UInt64(status),
|
||||
duration: endTime - startTime
|
||||
)
|
||||
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
private func sanitizeHeaders(headers: [String: String]?) -> [String: String]? {
|
||||
guard let headerContent = headers else { return nil }
|
||||
|
||||
var sanitizedHeaders = headerContent
|
||||
for key in ignoredKeys {
|
||||
if sanitizedHeaders.keys.contains(key) {
|
||||
sanitizedHeaders[key] = "***"
|
||||
}
|
||||
}
|
||||
return sanitizedHeaders
|
||||
}
|
||||
|
||||
|
||||
private func sanitizeBody(body: String?) -> String? {
|
||||
guard let bodyContent = body else { return nil }
|
||||
|
||||
var sanitizedBody = bodyContent
|
||||
for key in ignoredKeys {
|
||||
if let range = sanitizedBody.range(of: "\"\(key)\":\"[^\"]*\"", options: .regularExpression) {
|
||||
sanitizedBody.replaceSubrange(range, with: "\"\(key)\":\"***\"")
|
||||
}
|
||||
}
|
||||
return sanitizedBody
|
||||
}
|
||||
}
|
||||
|
||||
func convertDictionaryToJSONString(dictionary: [String: Any?]) -> String? {
|
||||
if let jsonData = try? JSONSerialization.data(withJSONObject: dictionary, options: []) {
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func transformHeaders(_ headers: [AnyHashable: Any]) -> [String: String] {
|
||||
var stringHeaders: [String: String] = [:]
|
||||
for (key, value) in headers {
|
||||
if let stringKey = key.base as? String, let stringValue = value as? String {
|
||||
stringHeaders[stringKey] = stringValue
|
||||
}
|
||||
}
|
||||
return stringHeaders
|
||||
}
|
||||
|
||||
|
||||
func isJSONString(string: String) -> Bool {
|
||||
if let data = string.data(using: .utf8) {
|
||||
do {
|
||||
_ = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
return true
|
||||
} catch {
|
||||
DebugUtils.log("Error: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
open class PerformanceListener: NSObject {
|
||||
public static let shared = PerformanceListener()
|
||||
private var cpuTimer: Timer?
|
||||
private var cpuIteration = 0
|
||||
private var memTimer: Timer?
|
||||
public var isActive = false
|
||||
|
||||
func start() {
|
||||
#warning("Can interfere with client usage")
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
|
||||
let observe: (Notification.Name) -> Void = {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.notified(_:)), name: $0, object: nil)
|
||||
}
|
||||
observe(.NSBundleResourceRequestLowDiskSpace)
|
||||
observe(.NSProcessInfoPowerStateDidChange)
|
||||
observe(ProcessInfo.thermalStateDidChangeNotification)
|
||||
observe(UIApplication.didReceiveMemoryWarningNotification)
|
||||
observe(UIDevice.batteryLevelDidChangeNotification)
|
||||
observe(UIDevice.batteryStateDidChangeNotification)
|
||||
observe(UIDevice.orientationDidChangeNotification)
|
||||
|
||||
getCpuMessage()
|
||||
getMemoryMessage()
|
||||
|
||||
cpuTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { (_) in
|
||||
self.getCpuMessage()
|
||||
})
|
||||
|
||||
memTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true, block: { (_) in
|
||||
self.getMemoryMessage()
|
||||
})
|
||||
isActive = true
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(resume), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc func resume() {
|
||||
#if DEBUG
|
||||
DebugUtils.log("Resume")
|
||||
#endif
|
||||
getCpuMessage()
|
||||
getMemoryMessage()
|
||||
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "background", value: UInt64(0)))
|
||||
}
|
||||
|
||||
@objc func pause() {
|
||||
#if DEBUG
|
||||
DebugUtils.log("Background")
|
||||
#endif
|
||||
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "background", value: UInt64(1)))
|
||||
}
|
||||
|
||||
func getCpuMessage() {
|
||||
if let cpu = self.cpuUsage() {
|
||||
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "mainThreadCPU", value: UInt64(cpu)))
|
||||
}
|
||||
}
|
||||
|
||||
func getMemoryMessage() {
|
||||
if let mem = self.memoryUsage() {
|
||||
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "memoryUsage", value: UInt64(mem)))
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
if isActive {
|
||||
UIDevice.current.isBatteryMonitoringEnabled = false
|
||||
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: .NSBundleResourceRequestLowDiskSpace, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .NSProcessInfoPowerStateDidChange, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: ProcessInfo.thermalStateDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.batteryLevelDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.batteryStateDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
cpuTimer?.invalidate()
|
||||
cpuTimer = nil
|
||||
memTimer?.invalidate()
|
||||
memTimer = nil
|
||||
|
||||
isActive = false
|
||||
}
|
||||
}
|
||||
|
||||
public func sendBattery() {
|
||||
let message = ORIOSPerformanceEvent(name: "batteryLevel", value: 20)
|
||||
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
public func sendThermal() {
|
||||
let message2 = ORIOSPerformanceEvent(name: "thermalState", value: 2)
|
||||
MessageCollector.shared.sendMessage(message2)
|
||||
}
|
||||
|
||||
@objc func notified(_ notification: Notification) {
|
||||
var message: ORIOSPerformanceEvent? = nil
|
||||
switch notification.name {
|
||||
case .NSBundleResourceRequestLowDiskSpace:
|
||||
message = ORIOSPerformanceEvent(name: "lowDiskSpace", value: 0)
|
||||
case .NSProcessInfoPowerStateDidChange:
|
||||
message = ORIOSPerformanceEvent(name: "isLowPowerModeEnabled", value: ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0)
|
||||
case ProcessInfo.thermalStateDidChangeNotification:
|
||||
message = ORIOSPerformanceEvent(name: "thermalState", value: UInt64(ProcessInfo.processInfo.thermalState.rawValue))
|
||||
case UIApplication.didReceiveMemoryWarningNotification:
|
||||
message = ORIOSPerformanceEvent(name: "memoryWarning", value: 0)
|
||||
case UIDevice.batteryLevelDidChangeNotification:
|
||||
message = ORIOSPerformanceEvent(name: "batteryLevel", value: UInt64(max(0.0, UIDevice.current.batteryLevel)*100))
|
||||
case UIDevice.batteryStateDidChangeNotification:
|
||||
message = ORIOSPerformanceEvent(name: "batteryState", value: UInt64(UIDevice.current.batteryState.rawValue))
|
||||
case UIDevice.orientationDidChangeNotification:
|
||||
message = ORIOSPerformanceEvent(name: "orientation", value: UInt64(UIDevice.current.orientation.rawValue))
|
||||
default: break
|
||||
}
|
||||
if let message = message {
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
func networkStateChange(_ state: UInt64) {
|
||||
let message = ORIOSPerformanceEvent(name: "networkState", value: state)
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
func memoryUsage() -> UInt64? {
|
||||
var taskInfo = task_vm_info_data_t()
|
||||
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info>.size) / 4
|
||||
let result: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
||||
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
|
||||
}
|
||||
}
|
||||
|
||||
guard result == KERN_SUCCESS else {
|
||||
return nil
|
||||
}
|
||||
return UInt64(taskInfo.phys_footprint)
|
||||
}
|
||||
|
||||
func cpuUsage() -> Double? {
|
||||
var threadsListContainer: thread_act_array_t?
|
||||
var threadsCount = mach_msg_type_number_t(0)
|
||||
let threadsResult = withUnsafeMutablePointer(to: &threadsListContainer) {
|
||||
return $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) {
|
||||
task_threads(mach_task_self_, $0, &threadsCount)
|
||||
}
|
||||
}
|
||||
defer {
|
||||
vm_deallocate(mach_task_self_, vm_address_t(UInt(bitPattern: threadsListContainer)), vm_size_t(Int(threadsCount) * MemoryLayout<thread_t>.stride))
|
||||
}
|
||||
|
||||
guard threadsCount > 0, threadsResult == KERN_SUCCESS, let threadsList = threadsListContainer else {
|
||||
return nil
|
||||
}
|
||||
var threadInfo = thread_basic_info()
|
||||
var threadInfoCount = mach_msg_type_number_t(THREAD_INFO_MAX)
|
||||
let infoResult = withUnsafeMutablePointer(to: &threadInfo) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
||||
thread_info(threadsList[0], thread_flavor_t(THREAD_BASIC_INFO), $0, &threadInfoCount)
|
||||
}
|
||||
}
|
||||
|
||||
let threadBasicInfo = threadInfo as thread_basic_info
|
||||
guard infoResult == KERN_SUCCESS, threadBasicInfo.flags & TH_FLAGS_IDLE == 0 else { return nil }
|
||||
return Double(threadBasicInfo.cpu_usage) / Double(TH_USAGE_SCALE) * 100.0
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
class DebugUtils: NSObject {
|
||||
|
||||
static func error(_ str: String) {
|
||||
// TODO: fix this one
|
||||
// MessageCollector.shared.sendMessage(ASIOSInternalError(content: str))
|
||||
log(str)
|
||||
}
|
||||
|
||||
static func log(_ str: String) {
|
||||
#if DEBUG
|
||||
print(str)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
struct BatchArch {
|
||||
var name: String
|
||||
var data: Data
|
||||
}
|
||||
|
||||
class MessageCollector: NSObject {
|
||||
public static let shared = MessageCollector()
|
||||
private var imagesWaiting = [BatchArch]()
|
||||
private var imagesSending = [BatchArch]()
|
||||
private var messagesWaiting = [Data]()
|
||||
private var nextMessageIndex = 0
|
||||
private var sendingLastMessages = false
|
||||
private let maxMessagesSize = 500_000
|
||||
private let messagesQueue = OperationQueue()
|
||||
private let lateMessagesFile: URL?
|
||||
private var sendInderval: Timer?
|
||||
|
||||
override init() {
|
||||
lateMessagesFile = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("/lateMessages.dat")
|
||||
super.init()
|
||||
}
|
||||
|
||||
func start() {
|
||||
sendInderval = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [weak self] _ in
|
||||
self?.flush()
|
||||
})
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(terminate), name: UIApplication.willResignActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(terminate), name: UIApplication.willTerminateNotification, object: nil)
|
||||
messagesQueue.maxConcurrentOperationCount = 1
|
||||
|
||||
if let fileUrl = lateMessagesFile,
|
||||
FileManager.default.fileExists(atPath: fileUrl.path),
|
||||
let lateData = try? Data(contentsOf: fileUrl) {
|
||||
NetworkManager.shared.sendLateMessage(content: lateData) { (success) in
|
||||
guard success else { return }
|
||||
try? FileManager.default.removeItem(at: fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
sendInderval?.invalidate()
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil)
|
||||
self.terminate()
|
||||
}
|
||||
|
||||
func sendImagesBatch(batch: Data, fileName: String) {
|
||||
self.imagesWaiting.append(BatchArch(name: fileName, data: batch))
|
||||
messagesQueue.addOperation {
|
||||
self.flushImages()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func terminate() {
|
||||
guard !sendingLastMessages else { return }
|
||||
messagesQueue.addOperation {
|
||||
self.sendingLastMessages = true
|
||||
self.flushMessages()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func flush() {
|
||||
messagesQueue.addOperation {
|
||||
self.flushMessages()
|
||||
self.flushImages()
|
||||
}
|
||||
}
|
||||
|
||||
private func flushImages() {
|
||||
let images = imagesWaiting.first
|
||||
guard !imagesWaiting.isEmpty, let images = images, let projectKey = ORTracker.shared.projectKey else { return }
|
||||
imagesWaiting.remove(at: 0)
|
||||
|
||||
imagesSending.append(images)
|
||||
|
||||
DebugUtils.log("Sending images \(images.name) \(images.data.count)")
|
||||
NetworkManager.shared.sendImages(projectKey: projectKey, images: images.data, name: images.name) { (success) in
|
||||
self.imagesSending.removeAll { (waiting) -> Bool in
|
||||
images.name == waiting.name
|
||||
}
|
||||
guard success else {
|
||||
self.imagesWaiting.append(images)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ message: ORMessage) {
|
||||
let data = message.contentData()
|
||||
#if DEBUG
|
||||
if !message.description.contains("IOSLog") && !message.description.contains("IOSNetworkCall") {
|
||||
DebugUtils.log(message.description)
|
||||
}
|
||||
if let networkCallMessage = message as? ORIOSNetworkCall {
|
||||
DebugUtils.log("-->> IOSNetworkCall(105): \(networkCallMessage.method) \(networkCallMessage.URL)")
|
||||
}
|
||||
#endif
|
||||
self.sendRawMessage(data)
|
||||
}
|
||||
|
||||
private var debounceTimer: Timer?
|
||||
private var debouncedMessage: ORMessage?
|
||||
func sendDebouncedMessage(_ message: ORMessage) {
|
||||
debounceTimer?.invalidate()
|
||||
|
||||
debouncedMessage = message
|
||||
debounceTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in
|
||||
if let debouncedMessage = self?.debouncedMessage {
|
||||
self?.sendMessage(debouncedMessage)
|
||||
self?.debouncedMessage = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendRawMessage(_ data: Data) {
|
||||
messagesQueue.addOperation {
|
||||
if data.count > self.maxMessagesSize {
|
||||
DebugUtils.log("<><><>Single message size exceeded limit")
|
||||
return
|
||||
}
|
||||
self.messagesWaiting.append(data)
|
||||
var totalWaitingSize = 0
|
||||
self.messagesWaiting.forEach { totalWaitingSize += $0.count }
|
||||
if totalWaitingSize > Int(Double(self.maxMessagesSize) * 0.8) {
|
||||
self.flushMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func flushMessages() {
|
||||
var messages = [Data]()
|
||||
var sentSize = 0
|
||||
while let message = messagesWaiting.first, sentSize + message.count <= maxMessagesSize {
|
||||
messages.append(message)
|
||||
messagesWaiting.remove(at: 0)
|
||||
sentSize += message.count
|
||||
}
|
||||
guard !messages.isEmpty else { return }
|
||||
var content = Data()
|
||||
let index = ORIOSBatchMeta(firstIndex: UInt64(nextMessageIndex))
|
||||
content.append(index.contentData())
|
||||
DebugUtils.log(index.description)
|
||||
messages.forEach { (message) in
|
||||
content.append(message)
|
||||
}
|
||||
if sendingLastMessages, let fileUrl = lateMessagesFile {
|
||||
try? content.write(to: fileUrl)
|
||||
}
|
||||
nextMessageIndex += messages.count
|
||||
DebugUtils.log("messages batch \(content)")
|
||||
NetworkManager.shared.sendMessage(content: content) { (success) in
|
||||
guard success else {
|
||||
DebugUtils.log("<><>re-sending failed batch<><>")
|
||||
self.messagesWaiting.insert(contentsOf: messages, at: 0)
|
||||
return
|
||||
}
|
||||
if self.sendingLastMessages {
|
||||
self.sendingLastMessages = false
|
||||
if let fileUrl = self.lateMessagesFile, FileManager.default.fileExists(atPath: fileUrl.path) {
|
||||
try? FileManager.default.removeItem(at: fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import UIKit
|
||||
import SWCompression
|
||||
|
||||
let START_URL = "/v1/mobile/start"
|
||||
let INGEST_URL = "/v1/mobile/i"
|
||||
let LATE_URL = "/v1/mobile/late"
|
||||
let IMAGES_URL = "/v1/mobile/images"
|
||||
|
||||
class NetworkManager: NSObject {
|
||||
static let shared = NetworkManager()
|
||||
var baseUrl = "https://api.openreplay.com/ingest"
|
||||
public var sessionId: String? = nil
|
||||
private var token: String? = nil
|
||||
public var writeToFile = false
|
||||
#if DEBUG
|
||||
private let localFilePath = "/Users/nikitamelnikov/Desktop/session.dat"
|
||||
#endif
|
||||
|
||||
override init() {
|
||||
#if DEBUG
|
||||
if writeToFile, FileManager.default.fileExists(atPath: localFilePath) {
|
||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: localFilePath))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func createRequest(method: String, path: String) -> URLRequest {
|
||||
let url = URL(string: baseUrl+path)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
return request
|
||||
}
|
||||
|
||||
private func callAPI(request: URLRequest,
|
||||
onSuccess: @escaping (Data) -> Void,
|
||||
onError: @escaping (Error?) -> Void) {
|
||||
guard !writeToFile else { return }
|
||||
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
|
||||
|
||||
DebugUtils.log(">>>\(request.httpMethod ?? ""):\(request.url?.absoluteString ?? "")\n<<<\(String(data: data ?? Data(), encoding: .utf8) ?? "")")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let data = data,
|
||||
let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
DebugUtils.error(">>>>>> Error in call \(request.url?.absoluteString ?? "") : \(error?.localizedDescription ?? "N/A")")
|
||||
if (response as? HTTPURLResponse)?.statusCode == 401 {
|
||||
self.token = nil
|
||||
ORTracker.shared.startSession(projectKey: ORTracker.shared.projectKey ?? "", options: ORTracker.shared.options)
|
||||
}
|
||||
onError(error)
|
||||
return
|
||||
}
|
||||
onSuccess(data)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func createSession(params: [String: AnyHashable], completion: @escaping (ORSessionResponse?) -> Void) {
|
||||
guard !writeToFile else {
|
||||
self.token = "writeToFile"
|
||||
return
|
||||
}
|
||||
var request = createRequest(method: "POST", path: START_URL)
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: params, options: []) else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
request.httpBody = jsonData
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
callAPI(request: request) { (data) in
|
||||
do {
|
||||
let session = try JSONDecoder().decode(ORSessionResponse.self, from: data)
|
||||
|
||||
self.token = session.token
|
||||
self.sessionId = session.sessionID
|
||||
ORUserDefaults.shared.lastToken = self.token
|
||||
|
||||
completion(session)
|
||||
} catch {
|
||||
DebugUtils.log("Can't unwrap session start resp: \(error)")
|
||||
}
|
||||
} onError: { _ in
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(content: Data, completion: @escaping (Bool) -> Void) {
|
||||
guard !writeToFile else {
|
||||
appendLocalFile(data: content)
|
||||
return
|
||||
}
|
||||
var request = createRequest(method: "POST", path: INGEST_URL)
|
||||
guard let token = token else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
var compressedContent = content
|
||||
let oldSize = compressedContent.count
|
||||
var newSize = oldSize
|
||||
do {
|
||||
let compressed = try GzipArchive.archive(data: content)
|
||||
compressedContent = compressed
|
||||
newSize = compressed.count
|
||||
request.setValue("gzip", forHTTPHeaderField: "Content-Encoding")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
DebugUtils.log(">>>>Compress batch file \(oldSize)>\(newSize)")
|
||||
} catch {
|
||||
DebugUtils.log("Error with compression: \(error)")
|
||||
}
|
||||
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
request.httpBody = compressedContent
|
||||
callAPI(request: request) { (data) in
|
||||
completion(true)
|
||||
} onError: { _ in
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
func sendLateMessage(content: Data, completion: @escaping (Bool) -> Void) {
|
||||
DebugUtils.log(">>>sending late messages")
|
||||
var request = createRequest(method: "POST", path: LATE_URL)
|
||||
guard let token = ORUserDefaults.shared.lastToken else {
|
||||
completion(false)
|
||||
DebugUtils.log("! No last token found")
|
||||
return
|
||||
}
|
||||
print(token)
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.httpBody = content
|
||||
callAPI(request: request) { (data) in
|
||||
completion(true)
|
||||
DebugUtils.log("<<< late messages sent")
|
||||
} onError: { _ in
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
func sendImages(projectKey: String, images: Data, name: String, completion: @escaping (Bool) -> Void) {
|
||||
var request = createRequest(method: "POST", path: IMAGES_URL)
|
||||
guard let token = token else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
let boundary = "Boundary-\(NSUUID().uuidString)"
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var body = Data()
|
||||
let parameters = ["projectKey": projectKey]
|
||||
for (key, value) in parameters {
|
||||
body.appendString("--\(boundary)\r\n")
|
||||
body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
|
||||
body.appendString("\(value)\r\n")
|
||||
}
|
||||
|
||||
body.appendString("--\(boundary)\r\n")
|
||||
body.appendString("Content-Disposition: form-data; name=\"batch\"; filename=\"\(name)\"\r\n")
|
||||
body.appendString("Content-Type: gzip\r\n\r\n")
|
||||
body.append(images)
|
||||
body.appendString("\r\n")
|
||||
|
||||
body.appendString("--\(boundary)--\r\n")
|
||||
DebugUtils.log(">>>>>> sending \(body.count) bytes")
|
||||
request.httpBody = body
|
||||
|
||||
callAPI(request: request) { (data) in
|
||||
completion(true)
|
||||
} onError: { _ in
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func appendLocalFile(data: Data) {
|
||||
#if DEBUG
|
||||
DebugUtils.log("appendInFile \(data.count) bytes")
|
||||
|
||||
let fileURL = URL(fileURLWithPath: localFilePath)
|
||||
if let fileHandle = try? FileHandle(forWritingTo: fileURL) {
|
||||
defer {
|
||||
fileHandle.closeFile()
|
||||
}
|
||||
fileHandle.seekToEndOfFile()
|
||||
fileHandle.write(data)
|
||||
} else {
|
||||
try? data.write(to: fileURL, options: .atomic)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
class ORUserDefaults: NSObject {
|
||||
public static let shared = ORUserDefaults()
|
||||
private let userDefaults: UserDefaults?
|
||||
|
||||
override init() {
|
||||
userDefaults = UserDefaults(suiteName: "io.orenreplay.openreplaytr-defaults")
|
||||
}
|
||||
|
||||
var userUUID: String {
|
||||
get {
|
||||
if let savedUUID = userDefaults?.string(forKey: "userUUID") {
|
||||
return savedUUID
|
||||
}
|
||||
let newUUID = UUID().uuidString
|
||||
self.userUUID = newUUID
|
||||
return newUUID
|
||||
}
|
||||
set {
|
||||
userDefaults?.set(newValue, forKey: "userUUID")
|
||||
}
|
||||
}
|
||||
|
||||
var lastToken: String? {
|
||||
get {
|
||||
return userDefaults?.string(forKey: "lastToken")
|
||||
}
|
||||
set {
|
||||
userDefaults?.set(newValue, forKey: "lastToken")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class Swizzling: NSObject {
|
||||
static func swizzle(cls: AnyClass, original: Selector, swizzled: Selector) {
|
||||
if let originalMethod = class_getInstanceMethod(cls, original),
|
||||
let swizzledMethod = class_getInstanceMethod(cls, swizzled) {
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}
|
||||
}
|
||||
static func swizzleIfPresent(cls: AnyClass, original: Selector, swizzled: Selector) {
|
||||
if let originalMethod = class_getInstanceMethod(cls, original),
|
||||
let swizzledMethod = class_getInstanceMethod(cls, swizzled) {
|
||||
let didAddMethod = class_addMethod(self, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
|
||||
if didAddMethod {
|
||||
class_replaceMethod(self, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
|
||||
} else {
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
struct GenericMessage {
|
||||
let typeRaw: UInt64
|
||||
let type: ORMessageType?
|
||||
let timestamp: UInt64
|
||||
let body: Data
|
||||
|
||||
init?(data: Data, offset: inout Int) {
|
||||
do {
|
||||
typeRaw = try data.readPrimary(offset: &offset)
|
||||
type = ORMessageType(rawValue: typeRaw)
|
||||
timestamp = try data.readPrimary(offset: &offset)
|
||||
body = try data.readData(offset: &offset)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ORMessage: NSObject {
|
||||
|
||||
let messageRaw: UInt64
|
||||
let message: ORMessageType?
|
||||
let timestamp: UInt64
|
||||
|
||||
init(messageType: ORMessageType) {
|
||||
self.messageRaw = messageType.rawValue
|
||||
self.message = messageType
|
||||
self.timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
|
||||
}
|
||||
|
||||
init?(genericMessage: GenericMessage) {
|
||||
self.messageRaw = genericMessage.typeRaw
|
||||
self.message = genericMessage.type
|
||||
self.timestamp = genericMessage.timestamp
|
||||
}
|
||||
|
||||
func contentData() -> Data {
|
||||
fatalError("This method should be overridden")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
@objc public enum RecordingQuality: Int {
|
||||
case Low
|
||||
case Standard
|
||||
case High
|
||||
}
|
||||
|
||||
open class OROptions: NSObject {
|
||||
let crashes: Bool
|
||||
let analytics: Bool
|
||||
let performances: Bool
|
||||
let logs: Bool
|
||||
let screen: Bool
|
||||
let wifiOnly: Bool
|
||||
|
||||
public static let defaults = OROptions(crashes: true, analytics: true, performances: true, logs: true, screen: true, wifiOnly: true)
|
||||
|
||||
@objc public init(crashes: Bool, analytics: Bool, performances: Bool, logs: Bool, screen: Bool, wifiOnly: Bool) {
|
||||
self.crashes = crashes
|
||||
self.analytics = analytics
|
||||
self.performances = performances
|
||||
self.logs = logs
|
||||
self.screen = screen
|
||||
self.wifiOnly = wifiOnly
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
|
||||
struct ORRecord: Codable {
|
||||
let img: String
|
||||
let originX: CGFloat
|
||||
let originY: CGFloat
|
||||
let timestamp: UInt64
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import UIKit
|
||||
import DeviceKit
|
||||
|
||||
class ORSessionRequest: NSObject {
|
||||
private static var params = [String: AnyHashable]()
|
||||
|
||||
static func create( completion: @escaping (ORSessionResponse?) -> Void) {
|
||||
guard let projectKey = ORTracker.shared.projectKey else { return print("Openreplay: no project key added") }
|
||||
#warning("Can interfere with client usage")
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
|
||||
let performances: [String: UInt64] = [
|
||||
"physicalMemory": UInt64(ProcessInfo.processInfo.physicalMemory),
|
||||
"processorCount": UInt64(ProcessInfo.processInfo.processorCount),
|
||||
"activeProcessorCount": UInt64(ProcessInfo.processInfo.activeProcessorCount),
|
||||
"systemUptime": UInt64(ProcessInfo.processInfo.systemUptime),
|
||||
"isLowPowerModeEnabled": UInt64(ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0),
|
||||
"thermalState": UInt64(ProcessInfo.processInfo.thermalState.rawValue),
|
||||
"batteryLevel": UInt64(max(0.0, UIDevice.current.batteryLevel)*100),
|
||||
"batteryState": UInt64(UIDevice.current.batteryState.rawValue),
|
||||
"orientation": UInt64(UIDevice.current.orientation.rawValue),
|
||||
]
|
||||
|
||||
let device = Device.current
|
||||
var deviceModel = ""
|
||||
var deviceSafeName = ""
|
||||
|
||||
if device.isSimulator {
|
||||
deviceSafeName = "iPhone 14 Pro"
|
||||
deviceModel = "iPhone14,8"
|
||||
} else {
|
||||
deviceSafeName = device.safeDescription
|
||||
deviceModel = Device.identifier
|
||||
}
|
||||
|
||||
DebugUtils.log(">>>> device \(device) type \(device.safeDescription) mem \(UInt64(ProcessInfo.processInfo.physicalMemory / 1024))")
|
||||
params = [
|
||||
"projectKey": projectKey,
|
||||
"trackerVersion": Bundle(for: ORTracker.shared.classForCoder).object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "N/A",
|
||||
"revID": Bundle(for: ORTracker.shared.classForCoder).object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "N/A",
|
||||
"userUUID": ORUserDefaults.shared.userUUID,
|
||||
"userOSVersion": UIDevice.current.systemVersion,
|
||||
"userDevice": deviceModel,
|
||||
"userDeviceType": deviceSafeName,
|
||||
"timestamp": UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
"performances": performances,
|
||||
"deviceMemory": UInt64(ProcessInfo.processInfo.physicalMemory / 1024),
|
||||
"timezone": getTimezone(),
|
||||
]
|
||||
callAPI(completion: completion)
|
||||
}
|
||||
|
||||
private static func callAPI(completion: @escaping (ORSessionResponse?) -> Void) {
|
||||
guard !params.isEmpty else { return }
|
||||
NetworkManager.shared.createSession(params: params) { (sessionResponse) in
|
||||
guard let sessionResponse = sessionResponse else {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
|
||||
callAPI(completion: completion)
|
||||
}
|
||||
return
|
||||
}
|
||||
DebugUtils.log(">>>> Starting session : \(sessionResponse.sessionID)")
|
||||
return completion(sessionResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ORSessionResponse: Decodable {
|
||||
let userUUID: String
|
||||
let token: String
|
||||
let imagesHashList: [String]?
|
||||
let sessionID: String
|
||||
let fps: Int
|
||||
let quality: String
|
||||
}
|
||||
|
||||
func getTimezone() -> String {
|
||||
let offset = TimeZone.current.secondsFromGMT()
|
||||
let sign = offset >= 0 ? "+" : "-"
|
||||
let hours = abs(offset) / 3600
|
||||
let minutes = (abs(offset) % 3600) / 60
|
||||
return String(format: "UTC%@%02d:%02d", sign, hours, minutes)
|
||||
}
|
||||
|
|
@ -1,499 +0,0 @@
|
|||
|
||||
// Auto-generated, do not edit
|
||||
import UIKit
|
||||
|
||||
enum ORMessageType: UInt64 {
|
||||
case iOSMetadata = 92
|
||||
case iOSEvent = 93
|
||||
case iOSUserID = 94
|
||||
case iOSUserAnonymousID = 95
|
||||
case iOSScreenChanges = 96
|
||||
case iOSCrash = 97
|
||||
case iOSViewComponentEvent = 98
|
||||
case iOSClickEvent = 100
|
||||
case iOSInputEvent = 101
|
||||
case iOSPerformanceEvent = 102
|
||||
case iOSLog = 103
|
||||
case iOSInternalError = 104
|
||||
case iOSNetworkCall = 105
|
||||
case iOSSwipeEvent = 106
|
||||
case iOSBatchMeta = 107
|
||||
}
|
||||
|
||||
class ORIOSMetadata: ORMessage {
|
||||
let key: String
|
||||
let value: String
|
||||
|
||||
init(key: String, value: String) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
super.init(messageType: .iOSMetadata)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.key = try genericMessage.body.readString(offset: &offset)
|
||||
self.value = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(92), timestamp, Data(values: key, value))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSMetadata(92): timestamp:\(timestamp) key:\(key) value:\(value)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSEvent: ORMessage {
|
||||
let name: String
|
||||
let payload: String
|
||||
|
||||
init(name: String, payload: String) {
|
||||
self.name = name
|
||||
self.payload = payload
|
||||
super.init(messageType: .iOSEvent)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.name = try genericMessage.body.readString(offset: &offset)
|
||||
self.payload = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(93), timestamp, Data(values: name, payload))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSEvent(93): timestamp:\(timestamp) name:\(name) payload:\(payload)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSUserID: ORMessage {
|
||||
let iD: String
|
||||
|
||||
init(iD: String) {
|
||||
self.iD = iD
|
||||
super.init(messageType: .iOSUserID)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.iD = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(94), timestamp, Data(values: iD))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSUserID(94): timestamp:\(timestamp) iD:\(iD)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSUserAnonymousID: ORMessage {
|
||||
let iD: String
|
||||
|
||||
init(iD: String) {
|
||||
self.iD = iD
|
||||
super.init(messageType: .iOSUserAnonymousID)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.iD = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(95), timestamp, Data(values: iD))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSUserAnonymousID(95): timestamp:\(timestamp) iD:\(iD)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSScreenChanges: ORMessage {
|
||||
let x: UInt64
|
||||
let y: UInt64
|
||||
let width: UInt64
|
||||
let height: UInt64
|
||||
|
||||
init(x: UInt64, y: UInt64, width: UInt64, height: UInt64) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
super.init(messageType: .iOSScreenChanges)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.x = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.y = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.width = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.height = try genericMessage.body.readPrimary(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(96), timestamp, Data(values: x, y, width, height))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSScreenChanges(96): timestamp:\(timestamp) x:\(x) y:\(y) width:\(width) height:\(height)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSCrash: ORMessage {
|
||||
let name: String
|
||||
let reason: String
|
||||
let stacktrace: String
|
||||
|
||||
init(name: String, reason: String, stacktrace: String) {
|
||||
self.name = name
|
||||
self.reason = reason
|
||||
self.stacktrace = stacktrace
|
||||
super.init(messageType: .iOSCrash)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.name = try genericMessage.body.readString(offset: &offset)
|
||||
self.reason = try genericMessage.body.readString(offset: &offset)
|
||||
self.stacktrace = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(97), timestamp, Data(values: name, reason, stacktrace))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSCrash(97): timestamp:\(timestamp) name:\(name) reason:\(reason) stacktrace:\(stacktrace)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSViewComponentEvent: ORMessage {
|
||||
let screenName: String
|
||||
let viewName: String
|
||||
let visible: Bool
|
||||
|
||||
init(screenName: String, viewName: String, visible: Bool) {
|
||||
self.screenName = screenName
|
||||
self.viewName = viewName
|
||||
self.visible = visible
|
||||
super.init(messageType: .iOSViewComponentEvent)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.screenName = try genericMessage.body.readString(offset: &offset)
|
||||
self.viewName = try genericMessage.body.readString(offset: &offset)
|
||||
self.visible = try genericMessage.body.readPrimary(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(98), timestamp, Data(values: screenName, viewName, visible))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSViewComponentEvent(98): timestamp:\(timestamp) screenName:\(screenName) viewName:\(viewName) visible:\(visible)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSClickEvent: ORMessage {
|
||||
let label: String
|
||||
let x: UInt64
|
||||
let y: UInt64
|
||||
|
||||
init(label: String, x: UInt64, y: UInt64) {
|
||||
self.label = label
|
||||
self.x = x
|
||||
self.y = y
|
||||
super.init(messageType: .iOSClickEvent)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.label = try genericMessage.body.readString(offset: &offset)
|
||||
self.x = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.y = try genericMessage.body.readPrimary(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(100), timestamp, Data(values: label, x, y))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSClickEvent(100): timestamp:\(timestamp) label:\(label) x:\(x) y:\(y)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSInputEvent: ORMessage {
|
||||
let value: String
|
||||
let valueMasked: Bool
|
||||
let label: String
|
||||
|
||||
init(value: String, valueMasked: Bool, label: String) {
|
||||
self.value = value
|
||||
self.valueMasked = valueMasked
|
||||
self.label = label
|
||||
super.init(messageType: .iOSInputEvent)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.value = try genericMessage.body.readString(offset: &offset)
|
||||
self.valueMasked = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.label = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(101), timestamp, Data(values: value, valueMasked, label))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSInputEvent(101): timestamp:\(timestamp) value:\(value) valueMasked:\(valueMasked) label:\(label)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSPerformanceEvent: ORMessage {
|
||||
let name: String
|
||||
let value: UInt64
|
||||
|
||||
init(name: String, value: UInt64) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
super.init(messageType: .iOSPerformanceEvent)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.name = try genericMessage.body.readString(offset: &offset)
|
||||
self.value = try genericMessage.body.readPrimary(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(102), timestamp, Data(values: name, value))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSPerformanceEvent(102): timestamp:\(timestamp) name:\(name) value:\(value)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSLog: ORMessage {
|
||||
let severity: String
|
||||
let content: String
|
||||
|
||||
init(severity: String, content: String) {
|
||||
self.severity = severity
|
||||
self.content = content
|
||||
super.init(messageType: .iOSLog)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.severity = try genericMessage.body.readString(offset: &offset)
|
||||
self.content = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(103), timestamp, Data(values: severity, content))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSLog(103): timestamp:\(timestamp) severity:\(severity) content:\(content)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSInternalError: ORMessage {
|
||||
let content: String
|
||||
|
||||
init(content: String) {
|
||||
self.content = content
|
||||
super.init(messageType: .iOSInternalError)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.content = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(104), timestamp, Data(values: content))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSInternalError(104): timestamp:\(timestamp) content:\(content)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSNetworkCall: ORMessage {
|
||||
let type: String
|
||||
let method: String
|
||||
let URL: String
|
||||
let request: String
|
||||
let response: String
|
||||
let status: UInt64
|
||||
let duration: UInt64
|
||||
|
||||
init(type: String, method: String, URL: String, request: String, response: String, status: UInt64, duration: UInt64) {
|
||||
self.type = type
|
||||
self.method = method
|
||||
self.URL = URL
|
||||
self.request = request
|
||||
self.response = response
|
||||
self.status = status
|
||||
self.duration = duration
|
||||
super.init(messageType: .iOSNetworkCall)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.type = try genericMessage.body.readString(offset: &offset)
|
||||
self.method = try genericMessage.body.readString(offset: &offset)
|
||||
self.URL = try genericMessage.body.readString(offset: &offset)
|
||||
self.request = try genericMessage.body.readString(offset: &offset)
|
||||
self.response = try genericMessage.body.readString(offset: &offset)
|
||||
self.status = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.duration = try genericMessage.body.readPrimary(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(105), timestamp, Data(values: type, method, URL, request, response, status, duration))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSNetworkCall(105): timestamp:\(timestamp) type:\(type) method:\(method) URL:\(URL) request:\(request) response:\(response) status:\(status) duration:\(duration)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSSwipeEvent: ORMessage {
|
||||
let label: String
|
||||
let x: UInt64
|
||||
let y: UInt64
|
||||
let direction: String
|
||||
|
||||
init(label: String, x: UInt64, y: UInt64, direction: String) {
|
||||
self.label = label
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.direction = direction
|
||||
super.init(messageType: .iOSSwipeEvent)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.label = try genericMessage.body.readString(offset: &offset)
|
||||
self.x = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.y = try genericMessage.body.readPrimary(offset: &offset)
|
||||
self.direction = try genericMessage.body.readString(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(106), timestamp, Data(values: label, x, y, direction))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSSwipeEvent(106): timestamp:\(timestamp) label:\(label) x:\(x) y:\(y) direction:\(direction)";
|
||||
}
|
||||
}
|
||||
|
||||
class ORIOSBatchMeta: ORMessage {
|
||||
let firstIndex: UInt64
|
||||
|
||||
init(firstIndex: UInt64) {
|
||||
self.firstIndex = firstIndex
|
||||
super.init(messageType: .iOSBatchMeta)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
do {
|
||||
var offset = 0
|
||||
self.firstIndex = try genericMessage.body.readPrimary(offset: &offset)
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(107), timestamp, Data(values: firstIndex))
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> IOSBatchMeta(107): timestamp:\(timestamp) firstIndex:\(firstIndex)";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
|
||||
// Auto-generated, do not edit
|
||||
import UIKit
|
||||
|
||||
enum ORMessageType: UInt64 {
|
||||
<%= $messages.map { |msg| " case #{msg.name.first_lower} = #{msg.id}" }.join "\n" %>
|
||||
}
|
||||
<% $messages.each do |msg| %>
|
||||
class OR<%= msg.name.to_s.camel_case %>: ORMessage {
|
||||
<%= msg.attributes[2..-1].map { |attr| " let #{attr.property}: #{attr.type_swift}" }.join "\n" %>
|
||||
|
||||
init(<%= msg.attributes[2..-1].map { |attr| "#{attr.property}: #{attr.type_swift}" }.join ", " %>) {
|
||||
<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = #{attr.property}" }.join "\n" %>
|
||||
super.init(messageType: .<%= "#{msg.name.first_lower}" %>)
|
||||
}
|
||||
|
||||
override init?(genericMessage: GenericMessage) {
|
||||
<% if msg.attributes.length > 2 %> do {
|
||||
var offset = 0
|
||||
<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = try genericMessage.body.read#{attr.type_swift_read}(offset: &offset)" }.join "\n" %>
|
||||
super.init(genericMessage: genericMessage)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
<% else %>
|
||||
super.init(genericMessage: genericMessage)
|
||||
<% end %>}
|
||||
|
||||
override func contentData() -> Data {
|
||||
return Data(values: UInt64(<%= "#{msg.id}"%>), timestamp<% if msg.attributes.length > 2 %>, Data(values: <%= msg.attributes[2..-1].map { |attr| attr.property }.join ", "%>)<% end %>)
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "-->> <%= msg.name.to_s.camel_case %>(<%= "#{msg.id}"%>): timestamp:\(timestamp) <%= msg.attributes[2..-1].map { |attr| "#{attr.property}:\\(#{attr.property})" }.join " "%>";
|
||||
}
|
||||
}
|
||||
<% end %>
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
message 92, 'IOSMetadata' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Key'
|
||||
string 'Value'
|
||||
end
|
||||
|
||||
message 93, 'IOSEvent' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Name'
|
||||
string 'Payload'
|
||||
end
|
||||
|
||||
message 94, 'IOSUserID' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'ID'
|
||||
end
|
||||
|
||||
message 95, 'IOSUserAnonymousID' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'ID'
|
||||
end
|
||||
|
||||
message 96, 'IOSScreenChanges' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
uint 'X'
|
||||
uint 'Y'
|
||||
uint 'Width'
|
||||
uint 'Height'
|
||||
end
|
||||
|
||||
message 97, 'IOSCrash' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Name'
|
||||
string 'Reason'
|
||||
string 'Stacktrace'
|
||||
end
|
||||
|
||||
message 98, 'IOSViewComponentEvent' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'ScreenName'
|
||||
string 'ViewName'
|
||||
boolean 'Visible'
|
||||
end
|
||||
|
||||
|
||||
message 100, 'IOSClickEvent' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Label'
|
||||
uint 'X'
|
||||
uint 'Y'
|
||||
end
|
||||
|
||||
message 101, 'IOSInputEvent' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Value'
|
||||
boolean 'ValueMasked'
|
||||
string 'Label'
|
||||
end
|
||||
|
||||
=begin
|
||||
Name/Value may be :
|
||||
"physicalMemory": Total memory in bytes
|
||||
"processorCount": Total processors in device
|
||||
"activeProcessorCount": Number of currently used processors
|
||||
"systemUptime": Elapsed time (in seconds) since last boot
|
||||
"isLowPowerModeEnabled": Possible values (1 or 0)
|
||||
"thermalState": Possible values (0:nominal 1:fair 2:serious 3:critical)
|
||||
"batteryLevel": Possible values (0 .. 100)
|
||||
"batteryState": Possible values (0:unknown 1:unplugged 2:charging 3:full)
|
||||
"orientation": Possible values (0unknown 1:portrait 2:portraitUpsideDown 3:landscapeLeft 4:landscapeRight 5:faceUp 6:faceDown)
|
||||
"mainThreadCPU": Possible values (0 .. 100)
|
||||
"memoryUsage": Used memory in bytes
|
||||
"fps": Frames per second
|
||||
=end
|
||||
message 102, 'IOSPerformanceEvent' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Name'
|
||||
uint 'Value'
|
||||
end
|
||||
|
||||
message 103, 'IOSLog' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Severity' # Possible values ("info", "error")
|
||||
string 'Content'
|
||||
end
|
||||
|
||||
message 104, 'IOSInternalError' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Content'
|
||||
end
|
||||
|
||||
message 105, 'IOSNetworkCall' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Type'
|
||||
string 'Method'
|
||||
string 'URL'
|
||||
string 'Request'
|
||||
string 'Response'
|
||||
uint 'Status'
|
||||
uint 'Duration'
|
||||
end
|
||||
|
||||
message 106, 'IOSSwipeEvent' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
string 'Label'
|
||||
uint 'X'
|
||||
uint 'Y'
|
||||
string 'Direction'
|
||||
end
|
||||
|
||||
message 107, 'IOSBatchMeta' do
|
||||
uint 'Timestamp'
|
||||
uint 'Length'
|
||||
uint 'FirstIndex'
|
||||
end
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
require 'erb'
|
||||
|
||||
class String
|
||||
def camel_case
|
||||
return self if self !~ /_/ && self =~ /[A-Z]+.*/
|
||||
split('_').map{|e| e.capitalize}.join.upperize
|
||||
end
|
||||
|
||||
def camel_case_lower
|
||||
self.split('_').inject([]) do |buffer, e|
|
||||
word = if e.downcase == "url"
|
||||
"URL"
|
||||
else
|
||||
buffer.empty? ? e : e.capitalize
|
||||
end
|
||||
buffer.push(word)
|
||||
end.join.upperize
|
||||
end
|
||||
|
||||
def upperize
|
||||
self.sub('Id', 'ID').sub('Url', 'URL').sub('url', 'URL')
|
||||
end
|
||||
|
||||
def first_lower
|
||||
return "URL" if self == "URL"
|
||||
"" if self == ""
|
||||
self[0].downcase + self[1..-1]
|
||||
end
|
||||
|
||||
def underscore
|
||||
self.gsub(/::/, '/').
|
||||
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
||||
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
||||
tr("-", "_").
|
||||
downcase
|
||||
end
|
||||
end
|
||||
|
||||
class Attribute
|
||||
attr_reader :name, :type
|
||||
def initialize(name:, type:)
|
||||
@name = name
|
||||
@type = type
|
||||
end
|
||||
|
||||
def property
|
||||
@name.first_lower
|
||||
end
|
||||
|
||||
def type_swift
|
||||
case @type
|
||||
when :string
|
||||
"String"
|
||||
when :data
|
||||
"Data"
|
||||
when :uint
|
||||
"UInt64"
|
||||
when :boolean
|
||||
"Bool"
|
||||
else
|
||||
"Primary"
|
||||
end
|
||||
end
|
||||
|
||||
def type_swift_read
|
||||
case @type
|
||||
when :string
|
||||
"String"
|
||||
when :data
|
||||
"Data"
|
||||
else
|
||||
"Primary"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Message
|
||||
attr_reader :id, :name, :js, :replayer, :attributes
|
||||
def initialize(name:, id:, js: true, replayer: true, &block)
|
||||
@id = id
|
||||
@name = name
|
||||
@js = js
|
||||
@replayer = replayer
|
||||
@attributes = []
|
||||
instance_eval &block
|
||||
end
|
||||
|
||||
%i(uint string data boolean).each do |type|
|
||||
define_method type do |name, opts = {}|
|
||||
opts.merge!(
|
||||
name: name,
|
||||
type: type,
|
||||
)
|
||||
@attributes << Attribute.new(opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
$ids = []
|
||||
$messages = []
|
||||
def message(id, name, opts = {}, &block)
|
||||
raise "id duplicated #{name}" if $ids.include? id
|
||||
raise "id is too big #{name}" if id > 120
|
||||
$ids << id
|
||||
opts[:id] = id
|
||||
opts[:name] = name
|
||||
msg = Message.new(opts, &block)
|
||||
$messages << msg
|
||||
end
|
||||
|
||||
require './messages.rb'
|
||||
|
||||
e = ERB.new(File.read('./messages.erb'))
|
||||
File.write('ORMessages.swift', e.result)
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
import UIKit
|
||||
import Network
|
||||
|
||||
public enum CheckState {
|
||||
case unchecked
|
||||
case canStart
|
||||
case cantStart
|
||||
}
|
||||
|
||||
open class ORTracker: NSObject {
|
||||
@objc public static let shared = ORTracker()
|
||||
public let userDefaults = UserDefaults(suiteName: "io.asayer.AsayerSDK-defaults")
|
||||
public var projectKey: String?
|
||||
public var trackerState = CheckState.unchecked
|
||||
private var networkCheckTimer: Timer?
|
||||
public var serverURL: String {
|
||||
get { NetworkManager.shared.baseUrl }
|
||||
set { NetworkManager.shared.baseUrl = newValue }
|
||||
}
|
||||
public var options: OROptions = OROptions.defaults
|
||||
|
||||
@objc open func start(projectKey: String, options: OROptions) {
|
||||
self.options = options
|
||||
let monitor = NWPathMonitor()
|
||||
let q = DispatchQueue.global(qos: .background)
|
||||
|
||||
self.projectKey = projectKey
|
||||
|
||||
monitor.start(queue: q)
|
||||
|
||||
monitor.pathUpdateHandler = { path in
|
||||
if path.usesInterfaceType(.wifi) {
|
||||
if PerformanceListener.shared.isActive {
|
||||
PerformanceListener.shared.networkStateChange(1)
|
||||
}
|
||||
self.trackerState = CheckState.canStart
|
||||
} else if path.usesInterfaceType(.cellular) {
|
||||
if PerformanceListener.shared.isActive {
|
||||
PerformanceListener.shared.networkStateChange(0)
|
||||
}
|
||||
if options.wifiOnly {
|
||||
self.trackerState = CheckState.cantStart
|
||||
print("Connected to Cellular and options.wifiOnly is true. ORTracker will not start.")
|
||||
} else {
|
||||
self.trackerState = CheckState.canStart
|
||||
}
|
||||
} else {
|
||||
self.trackerState = CheckState.cantStart
|
||||
print("Not connected to either WiFi or Cellular. ORTracker will not start.")
|
||||
}
|
||||
}
|
||||
|
||||
networkCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (_) in
|
||||
if self.trackerState == CheckState.canStart {
|
||||
self.startSession(projectKey: projectKey, options: options)
|
||||
self.networkCheckTimer?.invalidate()
|
||||
}
|
||||
if self.trackerState == CheckState.cantStart {
|
||||
self.networkCheckTimer?.invalidate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@objc open func startSession(projectKey: String, options: OROptions) {
|
||||
self.projectKey = projectKey
|
||||
ORSessionRequest.create() { sessionResponse in
|
||||
guard let sessionResponse = sessionResponse else { return print("Openreplay: no response from /start request") }
|
||||
let captureSettings = getCaptureSettings(fps: sessionResponse.fps, quality: sessionResponse.quality)
|
||||
ScreenshotManager.shared.setSettings(settings: captureSettings)
|
||||
|
||||
MessageCollector.shared.start()
|
||||
|
||||
if options.logs {
|
||||
LogsListener.shared.start()
|
||||
}
|
||||
|
||||
if options.crashes {
|
||||
Crashs.shared.start()
|
||||
}
|
||||
|
||||
if options.performances {
|
||||
PerformanceListener.shared.start()
|
||||
}
|
||||
|
||||
if options.screen {
|
||||
ScreenshotManager.shared.start()
|
||||
}
|
||||
|
||||
if options.analytics {
|
||||
Analytics.shared.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc open func stop() {
|
||||
MessageCollector.shared.stop()
|
||||
ScreenshotManager.shared.stop()
|
||||
Crashs.shared.stop()
|
||||
PerformanceListener.shared.stop()
|
||||
Analytics.shared.stop()
|
||||
}
|
||||
|
||||
@objc open func addIgnoredView(_ view: UIView) {
|
||||
ScreenshotManager.shared.addSanitizedElement(view)
|
||||
}
|
||||
|
||||
@objc open func setMetadata(key: String, value: String) {
|
||||
let message = ORIOSMetadata(key: key, value: value)
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
@objc open func event(name: String, object: NSObject?) {
|
||||
event(name: name, payload: object as? Encodable)
|
||||
}
|
||||
|
||||
open func event(name: String, payload: Encodable?) {
|
||||
var json = ""
|
||||
if let payload = payload,
|
||||
let data = payload.toJSONData(),
|
||||
let jsonStr = String(data: data, encoding: .utf8) {
|
||||
json = jsonStr
|
||||
}
|
||||
let message = ORIOSEvent(name: name, payload: json)
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
open func eventStr(name: String, payload: String?) {
|
||||
let message = ORIOSEvent(name: name, payload: payload ?? "")
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
@objc open func setUserID(_ userID: String) {
|
||||
let message = ORIOSUserID(iD: userID)
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
@objc open func userAnonymousID(_ userID: String) {
|
||||
let message = ORIOSUserAnonymousID(iD: userID)
|
||||
MessageCollector.shared.sendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func getCaptureSettings(fps: Int, quality: String) -> (captureRate: Double, imgCompression: Double) {
|
||||
let limitedFPS = min(max(fps, 1), 99)
|
||||
let captureRate = 1.0 / Double(limitedFPS)
|
||||
|
||||
var imgCompression: Double
|
||||
switch quality.lowercased() {
|
||||
case "low":
|
||||
imgCompression = 0.4
|
||||
case "standard":
|
||||
imgCompression = 0.5
|
||||
case "high":
|
||||
imgCompression = 0.6
|
||||
default:
|
||||
imgCompression = 0.5 // default to standard if quality string is not recognized
|
||||
}
|
||||
|
||||
return (captureRate: captureRate, imgCompression: imgCompression)
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
import UIKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SWCompression
|
||||
|
||||
// MARK: - screenshot manager
|
||||
open class ScreenshotManager {
|
||||
public static let shared = ScreenshotManager()
|
||||
private let messagesQueue = OperationQueue()
|
||||
|
||||
private var timer: Timer?
|
||||
private var sendTimer: Timer?
|
||||
|
||||
private var sanitizedElements: [Sanitizable] = []
|
||||
private var observedInputs: [UITextField] = []
|
||||
private var screenshots: [Data] = []
|
||||
private var lastIndex = 0
|
||||
// MARK: capture settings
|
||||
// should we blur out sensitive views, or place a solid box on top
|
||||
private var isBlurMode = true
|
||||
private var blurRadius = 2.5
|
||||
// this affects how big the image will be compared to real phone screan.
|
||||
// we also can use default UIScreen.main.scale which is around 3.0 (dense pixel screen)
|
||||
private var screenScale = 1.25
|
||||
private var settings: (captureRate: Double, imgCompression: Double) = (captureRate: 0.33, imgCompression: 0.5)
|
||||
|
||||
private init() { }
|
||||
|
||||
func start() {
|
||||
startTakingScreenshots(every: settings.captureRate)
|
||||
}
|
||||
|
||||
func setSettings(settings: (captureRate: Double, imgCompression: Double)) {
|
||||
self.settings = settings
|
||||
}
|
||||
|
||||
func stop() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
lastIndex = 0
|
||||
screenshots.removeAll()
|
||||
}
|
||||
|
||||
func startTakingScreenshots(every interval: TimeInterval) {
|
||||
takeScreenshot()
|
||||
|
||||
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
||||
self?.takeScreenshot()
|
||||
}
|
||||
}
|
||||
|
||||
public func addSanitizedElement(_ element: Sanitizable) {
|
||||
#if DEBUG
|
||||
DebugUtils.log("called add")
|
||||
#endif
|
||||
sanitizedElements.append(element)
|
||||
}
|
||||
|
||||
public func removeSanitizedElement(_ element: Sanitizable) {
|
||||
#if DEBUG
|
||||
DebugUtils.log("called remove")
|
||||
#endif
|
||||
sanitizedElements.removeAll { $0 as AnyObject === element as AnyObject }
|
||||
}
|
||||
|
||||
// MARK: - UI Capturing
|
||||
func takeScreenshot() {
|
||||
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
|
||||
let size = window?.frame.size ?? CGSize.zero
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, screenScale)
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
|
||||
// Rendering current window in custom context
|
||||
// 2nd option looks to be more precise
|
||||
// window?.layer.render(in: context)
|
||||
#warning("Can slow down the app depending on complexity of the UI tree")
|
||||
window?.drawHierarchy(in: window?.bounds ?? CGRect.zero, afterScreenUpdates: false)
|
||||
|
||||
// MARK: sanitize
|
||||
// Sanitizing sensitive elements
|
||||
if isBlurMode {
|
||||
let stripeWidth: CGFloat = 5.0
|
||||
let stripeSpacing: CGFloat = 15.0
|
||||
let stripeColor: UIColor = .gray.withAlphaComponent(0.7)
|
||||
|
||||
for element in sanitizedElements {
|
||||
if let frame = element.frameInWindow {
|
||||
let totalWidth = frame.size.width
|
||||
let totalHeight = frame.size.height
|
||||
let convertedFrame = CGRect(
|
||||
x: frame.origin.x,
|
||||
y: frame.origin.y,
|
||||
width: frame.size.width,
|
||||
height: frame.size.height
|
||||
)
|
||||
let cropFrame = CGRect(
|
||||
x: frame.origin.x * screenScale,
|
||||
y: frame.origin.y * screenScale,
|
||||
width: frame.size.width * screenScale,
|
||||
height: frame.size.height * screenScale
|
||||
)
|
||||
if let regionImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage?.cropping(to: cropFrame) {
|
||||
let imageToBlur = UIImage(cgImage: regionImage, scale: screenScale, orientation: .up)
|
||||
let blurredImage = imageToBlur.applyBlurWithRadius(blurRadius)
|
||||
blurredImage?.draw(in: convertedFrame)
|
||||
|
||||
context.saveGState()
|
||||
UIRectClip(convertedFrame)
|
||||
|
||||
// Draw diagonal lines within the clipped region
|
||||
for x in stride(from: -totalHeight, to: totalWidth, by: stripeSpacing + stripeWidth) {
|
||||
context.move(to: CGPoint(x: x + convertedFrame.minX, y: convertedFrame.minY))
|
||||
context.addLine(to: CGPoint(x: x + totalHeight + convertedFrame.minX, y: totalHeight + convertedFrame.minY))
|
||||
}
|
||||
|
||||
context.setLineWidth(stripeWidth)
|
||||
stripeColor.setStroke()
|
||||
context.strokePath()
|
||||
context.restoreGState()
|
||||
|
||||
#if DEBUG
|
||||
context.setStrokeColor(UIColor.black.cgColor)
|
||||
context.setLineWidth(1)
|
||||
context.stroke(convertedFrame)
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
removeSanitizedElement(element)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context.setFillColor(UIColor.blue.cgColor)
|
||||
for element in sanitizedElements {
|
||||
if let frame = element.frameInWindow {
|
||||
context.fill(frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the resulting image
|
||||
if let image = UIGraphicsGetImageFromCurrentImageContext() {
|
||||
if let compressedData = image.jpegData(compressionQuality: self.settings.imgCompression) {
|
||||
screenshots.append(compressedData)
|
||||
if screenshots.count >= 10 {
|
||||
self.sendScreenshots()
|
||||
}
|
||||
}
|
||||
}
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
// Not using this because no idea how to sync it with the replay fps rn
|
||||
//func onError() {
|
||||
// takeScreenshot()
|
||||
//}
|
||||
|
||||
// MARK: - sending screenshots
|
||||
func sendScreenshots() {
|
||||
guard let sessionId = NetworkManager.shared.sessionId else {
|
||||
return
|
||||
}
|
||||
let localFilePath = "/Users/nikitamelnikov/Desktop/session/"
|
||||
let desktopURL = URL(fileURLWithPath: localFilePath)
|
||||
var archiveName = "\(sessionId)-\(String(format: "%06d", self.lastIndex)).tar.gz"
|
||||
let archiveURL = desktopURL.appendingPathComponent(archiveName)
|
||||
|
||||
// Ensure the directory exists
|
||||
let fileManager = FileManager.default
|
||||
if !fileManager.fileExists(atPath: localFilePath) {
|
||||
try? fileManager.createDirectory(at: desktopURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
var combinedData = Data()
|
||||
let images = screenshots
|
||||
for (index, imageData) in screenshots.enumerated() {
|
||||
combinedData.append(imageData)
|
||||
#if DEBUG
|
||||
let filename = "\(lastIndex)_\(index).jpeg"
|
||||
let fileURL = desktopURL.appendingPathComponent(filename)
|
||||
|
||||
do {
|
||||
try imageData.write(to: fileURL)
|
||||
} catch {
|
||||
DebugUtils.log("Unexpected error: \(error).")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if DEBUG
|
||||
DebugUtils.log("saved image files in \(localFilePath)")
|
||||
#endif
|
||||
|
||||
messagesQueue.addOperation {
|
||||
var entries: [TarEntry] = []
|
||||
for imageData in images {
|
||||
let filename = "\(String(format: "%06d", self.lastIndex)).jpeg"
|
||||
var tarEntry = TarContainer.Entry(info: .init(name: filename, type: .regular), data: imageData)
|
||||
tarEntry.info.permissions = Permissions(rawValue: 420)
|
||||
tarEntry.info.creationTime = Date()
|
||||
tarEntry.info.modificationTime = Date()
|
||||
|
||||
entries.append(tarEntry)
|
||||
self.lastIndex+=1
|
||||
}
|
||||
do {
|
||||
let gzData = try GzipArchive.archive(data: TarContainer.create(from: entries))
|
||||
|
||||
#if DEBUG
|
||||
try gzData.write(to: archiveURL)
|
||||
DebugUtils.log("Archive saved to \(archiveURL.path)")
|
||||
MessageCollector.shared.sendImagesBatch(batch: gzData, fileName: archiveName)
|
||||
#else
|
||||
MessageCollector.shared.sendImagesBatch(batch: gzData, fileName: archiveName)
|
||||
#endif
|
||||
} catch {
|
||||
DebugUtils.log("Error writing tar.gz data: \(error)")
|
||||
}
|
||||
}
|
||||
screenshots.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: making extensions for UI
|
||||
struct SensitiveViewWrapperRepresentable: UIViewRepresentable {
|
||||
@Binding var viewWrapper: SensitiveViewWrapper?
|
||||
|
||||
func makeUIView(context: Context) -> SensitiveViewWrapper {
|
||||
let wrapper = SensitiveViewWrapper()
|
||||
viewWrapper = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: SensitiveViewWrapper, context: Context) { }
|
||||
}
|
||||
|
||||
struct SensitiveModifier: ViewModifier {
|
||||
@State private var viewWrapper: SensitiveViewWrapper?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(SensitiveViewWrapperRepresentable(viewWrapper: $viewWrapper))
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func sensitive() -> some View {
|
||||
self.modifier(SensitiveModifier())
|
||||
}
|
||||
}
|
||||
|
||||
class SensitiveViewWrapper: UIView {
|
||||
override func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
if self.superview != nil {
|
||||
ScreenshotManager.shared.addSanitizedElement(self)
|
||||
} else {
|
||||
ScreenshotManager.shared.removeSanitizedElement(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SensitiveTextField: UITextField {
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if self.window != nil {
|
||||
ScreenshotManager.shared.addSanitizedElement(self)
|
||||
} else {
|
||||
ScreenshotManager.shared.removeSanitizedElement(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol to make a UIView sanitizable
|
||||
public protocol Sanitizable {
|
||||
var frameInWindow: CGRect? { get }
|
||||
}
|
||||
|
||||
|
||||
func getCaptureSettings(for quality: RecordingQuality) -> (captureRate: Double, imgCompression: Double) {
|
||||
switch quality {
|
||||
case .Low:
|
||||
return (captureRate: 1, imgCompression: 0.4)
|
||||
case .Standard:
|
||||
return (captureRate: 0.33, imgCompression: 0.5)
|
||||
case .High:
|
||||
return (captureRate: 0.20, imgCompression: 0.55)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import XCTest
|
||||
@testable import ORTracker
|
||||
|
||||
final class ORTrackerTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||
// results.
|
||||
// XCTAssertEqual(ORTracker().text, "Hello, World!")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue