Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ The diagnostics will categorize issues as:
The result object returned by `postInstallSearchLink()` and `extractLinkFromURL()` contains:

- `url: URL?` - The extracted deep link URL to navigate to
- `match_type: MatchType` - How the link was detected (`.unique`, `.default`, `.none`, `.unknown`)
- `matchType: MatchType` - How the link was detected (`.unique`, `.heuristics`, `.ambiguous`, `.intent`, `.none`)
- `analytics: [TracebackAnalyticsEvent]` - Analytics events you can send to your preferred platform

### TracebackConfiguration
Expand Down Expand Up @@ -283,4 +283,4 @@ do {
- Test on physical devices for Universal Links validation
- Use different network conditions
- Test both fresh installs and existing app scenarios
- Verify clipboard functionality works as expected
- Verify clipboard functionality works as expected
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,30 @@ struct APIProvider: Sendable {
throw NetworkError(error: error)
}
}

func getCampaignLink(from url: String, isFirstCampaignOpen: Bool) async throws -> CampaignResponse {
var urlComponents = URLComponents(
url: config.host.appendingPathComponent("v1_get_campaign", isDirectory: false),
resolvingAgainstBaseURL: false
)

// Add query parameters
urlComponents?.queryItems = [
URLQueryItem(name: "link", value: url),
URLQueryItem(name: "isFirstCampaignOpen", value: String(isFirstCampaignOpen))
]

guard let url = urlComponents?.url else {
throw NetworkError.unknown
}

var request = URLRequest(url: url)
request.httpMethod = "GET"

do {
return try await network.fetch(CampaignResponse.self, request: request)
} catch {
throw NetworkError(error: error)
}
}
}
13 changes: 13 additions & 0 deletions Sources/Traceback/Private/API/CampaignResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// CampaignResponse.swift
// traceback-ios
//
// Created by Nacho Sánchez on 10/24/25.
//

import Foundation

struct CampaignResponse: Decodable, Equatable, Sendable {
let result: URL?
let error: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct PostInstallLinkSearchResponse: Decodable, Equatable, Sendable {
let deep_link_id: URL?
let match_message: String
let match_type: String
let match_campaign: String?
let request_ip_version: String
let utm_medium: String?
let utm_source: String?
Expand All @@ -27,6 +28,8 @@ extension PostInstallLinkSearchResponse {
return .ambiguous
case "none":
return .none
case "intent":
return .intent
default:
return .unknown
}
Expand Down
56 changes: 56 additions & 0 deletions Sources/Traceback/Private/CampaignTracker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// CampaignTracker.swift
// Traceback
//
// Created by Nacho Sanchez on 24/10/25.
//

import Foundation

class CampaignTracker {
private let userDefaults: UserDefaults
private let seenCampaignsKey = "_traceback_seen_campaigns"

init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}

// Get all seen campaigns
private func getSeenCampaigns() -> Set<String> {
if let campaigns = userDefaults.array(forKey: seenCampaignsKey) as? [String] {
return Set(campaigns)
}
return Set()
}

// Save seen campaigns
private func saveSeenCampaigns(_ campaigns: Set<String>) {
userDefaults.set(Array(campaigns), forKey: seenCampaignsKey)
}

// Check if campaign has been seen before
func hasSeenCampaign(_ campaign: String) -> Bool {
return getSeenCampaigns().contains(campaign)
}

// Mark campaign as seen
func markCampaignAsSeen(_ campaign: String) {
var campaigns = getSeenCampaigns()
campaigns.insert(campaign)
saveSeenCampaigns(campaigns)
}

// Check and mark in one operation (returns true if first time)
func isFirstTimeSeen(_ campaign: String) -> Bool {
let isFirstTime = !hasSeenCampaign(campaign)
if isFirstTime {
markCampaignAsSeen(campaign)
}
return isFirstTime
}

// Clear all seen campaigns (useful for testing)
func clearSeenCampaigns() {
userDefaults.removeObject(forKey: seenCampaignsKey)
}
}
6 changes: 3 additions & 3 deletions Sources/Traceback/Private/DependenciesImpl/LoggerImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ extension Logger {
return Logger(
info: { message in
guard level == .info || level == .debug else { return }
logger.info("\(message())")
logger.info("\(message(), privacy: .public)")
},
debug: { message in
guard level == .debug else { return }
logger.debug("\(message())")
logger.debug("\(message(), privacy: .public)")
},
error: { message in
logger.error("\(message())")
logger.error("\(message(), privacy: .public)")
}
)
}
Expand Down
12 changes: 1 addition & 11 deletions Sources/Traceback/Private/DependenciesImpl/SystemInfoImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ enum TracebackSystemImpl {
SystemInfo(
installationTime: installationTime(),
deviceModelName: deviceModelName(),
sdkVersion: sdkVersion(),
sdkVersion: TracebackSDK.sdkVersion,
localeIdentifier: Locale.preferredLanguages.first ?? Locale.current.identifier,
timezone: TimeZone.current,
osVersion: UIDevice.current.systemVersion,
Expand Down Expand Up @@ -49,14 +49,4 @@ enum TracebackSystemImpl {
}
#endif
}

private static func sdkVersion() -> String {
guard let infoDictSDKVersion = Bundle(for: TracebackSDKImpl.self)
.infoDictionary?["CFBundleShortVersionString"] as? String else {
// fails in unit test
// assertionFailure()
return "1.0.0"
}
return infoDictSDKVersion
}
}
3 changes: 3 additions & 0 deletions Sources/Traceback/Private/DeviceFingerprint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct DeviceFingerprint: Codable, Equatable, Sendable {
let osVersion: String
let sdkVersion: String
let uniqueMatchLinkToCheck: URL?
let intentLink: URL?
let device: DeviceInfo

struct DeviceInfo: Codable, Equatable, Sendable {
Expand All @@ -33,6 +34,7 @@ struct DeviceFingerprint: Codable, Equatable, Sendable {
func createDeviceFingerprint(
system: SystemInfo,
linkFromClipboard: URL?,
linkFromIntent: URL?,
webviewInfo: WebViewInfo?
) -> DeviceFingerprint {

Expand Down Expand Up @@ -65,6 +67,7 @@ func createDeviceFingerprint(
osVersion: system.osVersion,
sdkVersion: system.sdkVersion,
uniqueMatchLinkToCheck: linkFromClipboard,
intentLink: linkFromIntent,
device: deviceInfo
)
}
Loading