firebase log level

This commit is contained in:
oscarz
2024-08-29 18:25:13 +08:00
parent 8500300d18
commit 27c160beaf
1165 changed files with 122916 additions and 1 deletions

View File

@ -0,0 +1,76 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// An enumeration of time periods.
enum TimePeriod: Int, CaseIterable, Codable {
/// The raw value is the number of calendar days within each time period.
/// More types can be enabled in future iterations (i.e. `weekly = 7, monthly = 28`).
case daily = 1
/// The number of seconds in a given time period.
var timeInterval: TimeInterval {
Double(rawValue) * 86400 /* seconds in day */
}
}
/// A structure representing SDK usage.
struct Heartbeat: Codable, Equatable {
/// The version of the heartbeat.
private static let version: Int = 0
/// An anonymous string of information (i.e. user agent) to associate the heartbeat with.
let agent: String
/// The date when the heartbeat was recorded.
let date: Date
/// The heartbeat's model version.
let version: Int
/// An array of `TimePeriod`s that the heartbeat is tagged with. See `TimePeriod`.
///
/// Heartbeats represent anonymous data points that measure SDK usage in moving averages for
/// various time periods. Because a single heartbeat can help calculate moving averages for
/// multiple
/// time periods, this property serves to capture all the time periods that the heartbeat can
/// represent in
/// a moving average.
let timePeriods: [TimePeriod]
/// Designated initializer.
/// - Parameters:
/// - agent: An anonymous string of information to associate the heartbeat with.
/// - date: The date when the heartbeat was recorded.
/// - version: The heartbeat's version. Defaults to the current version.
init(agent: String,
date: Date,
timePeriods: [TimePeriod] = [],
version: Int = version) {
self.agent = agent
self.date = date
self.timePeriods = timePeriods
self.version = version
}
}
extension Heartbeat: HeartbeatsPayloadConvertible {
func makeHeartbeatsPayload() -> HeartbeatsPayload {
let userAgentPayloads = [
HeartbeatsPayload.UserAgentPayload(agent: agent, dates: [date]),
]
return HeartbeatsPayload(userAgentPayloads: userAgentPayloads)
}
}

View File

@ -0,0 +1,157 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// An object that provides API to log and flush heartbeats from a synchronized storage container.
public final class HeartbeatController {
/// Used for standardizing dates for calendar-day comparison.
private enum DateStandardizer {
private static let calendar: Calendar = {
var calendar = Calendar(identifier: .iso8601)
calendar.locale = Locale(identifier: "en_US_POSIX")
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
return calendar
}()
static func standardize(_ date: Date) -> (Date) {
return calendar.startOfDay(for: date)
}
}
/// The thread-safe storage object to log and flush heartbeats from.
private let storage: HeartbeatStorageProtocol
/// The max capacity of heartbeats to store in storage.
private let heartbeatsStorageCapacity: Int = 30
/// Current date provider. It is used for testability.
private let dateProvider: () -> Date
/// Used for standardizing dates for calendar-day comparison.
private static let dateStandardizer = DateStandardizer.self
/// Public initializer.
/// - Parameter id: The `id` to associate this controller's heartbeat storage with.
public convenience init(id: String) {
self.init(id: id, dateProvider: Date.init)
}
/// Convenience initializer. Mirrors the semantics of the public initializer with the added
/// benefit of
/// injecting a custom date provider for improved testability.
/// - Parameters:
/// - id: The id to associate this controller's heartbeat storage with.
/// - dateProvider: A date provider.
convenience init(id: String, dateProvider: @escaping () -> Date) {
let storage = HeartbeatStorage.getInstance(id: id)
self.init(storage: storage, dateProvider: dateProvider)
}
/// Designated initializer.
/// - Parameters:
/// - storage: A heartbeat storage container.
/// - dateProvider: A date provider. Defaults to providing the current date.
init(storage: HeartbeatStorageProtocol,
dateProvider: @escaping () -> Date = Date.init) {
self.storage = storage
self.dateProvider = { Self.dateStandardizer.standardize(dateProvider()) }
}
/// Asynchronously logs a new heartbeat, if needed.
///
/// - Note: This API is thread-safe.
/// - Parameter agent: The string agent (i.e. Firebase User Agent) to associate the logged
/// heartbeat with.
public func log(_ agent: String) {
let date = dateProvider()
storage.readAndWriteAsync { heartbeatsBundle in
var heartbeatsBundle = heartbeatsBundle ??
HeartbeatsBundle(capacity: self.heartbeatsStorageCapacity)
// Filter for the time periods where the last heartbeat to be logged for
// that time period was logged more than one time period (i.e. day) ago.
let timePeriods = heartbeatsBundle.lastAddedHeartbeatDates.filter { timePeriod, lastDate in
date.timeIntervalSince(lastDate) >= timePeriod.timeInterval
}
.map { timePeriod, _ in timePeriod }
if !timePeriods.isEmpty {
// A heartbeat should only be logged if there is a time period(s) to
// associate it with.
let heartbeat = Heartbeat(agent: agent, date: date, timePeriods: timePeriods)
heartbeatsBundle.append(heartbeat)
}
return heartbeatsBundle
}
}
/// Synchronously flushes heartbeats from storage into a heartbeats payload.
///
/// - Note: This API is thread-safe.
/// - Returns: The flushed heartbeats in the form of `HeartbeatsPayload`.
@discardableResult
public func flush() -> HeartbeatsPayload {
let resetTransform = { (heartbeatsBundle: HeartbeatsBundle?) -> HeartbeatsBundle? in
guard let oldHeartbeatsBundle = heartbeatsBundle else {
return nil // Storage was empty.
}
// The new value that's stored will use the old's cache to prevent the
// logging of duplicates after flushing.
return HeartbeatsBundle(
capacity: self.heartbeatsStorageCapacity,
cache: oldHeartbeatsBundle.lastAddedHeartbeatDates
)
}
do {
// Synchronously gets and returns the stored heartbeats, resetting storage
// using the given transform.
let heartbeatsBundle = try storage.getAndSet(using: resetTransform)
// If no heartbeats bundle was stored, return an empty payload.
return heartbeatsBundle?.makeHeartbeatsPayload() ?? HeartbeatsPayload.emptyPayload
} catch {
// If the operation throws, assume no heartbeat(s) were retrieved or set.
return HeartbeatsPayload.emptyPayload
}
}
/// Synchronously flushes the heartbeat for today.
///
/// If no heartbeat was logged today, the returned payload is empty.
///
/// - Note: This API is thread-safe.
/// - Returns: A heartbeats payload for the flushed heartbeat.
@discardableResult
public func flushHeartbeatFromToday() -> HeartbeatsPayload {
let todaysDate = dateProvider()
var todaysHeartbeat: Heartbeat?
storage.readAndWriteSync { heartbeatsBundle in
guard var heartbeatsBundle = heartbeatsBundle else {
return nil // Storage was empty.
}
todaysHeartbeat = heartbeatsBundle.removeHeartbeat(from: todaysDate)
return heartbeatsBundle
}
// Note that `todaysHeartbeat` is updated in the above read/write block.
if todaysHeartbeat != nil {
return todaysHeartbeat!.makeHeartbeatsPayload()
} else {
return HeartbeatsPayload.emptyPayload
}
}
}

View File

@ -0,0 +1,140 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#if DEBUG
import Foundation
/// A utility class intended to be used only in testing contexts.
@objc(FIRHeartbeatLoggingTestUtils)
@objcMembers
public class HeartbeatLoggingTestUtils: NSObject {
/// This should mirror the `Constants` enum in the `HeartbeatLogging` module.
/// See `HeartbeatLogging/Sources/StorageFactory.swift`.
public enum Constants {
/// The name of the file system directory where heartbeat data is stored.
public static let heartbeatFileStorageDirectoryPath = "google-heartbeat-storage"
/// The name of the user defaults suite where heartbeat data is stored.
public static let heartbeatUserDefaultsSuiteName = "com.google.heartbeat.storage"
}
public static var dateFormatter: DateFormatter {
HeartbeatsPayload.dateFormatter
}
public static var emptyHeartbeatsPayload: _ObjC_HeartbeatsPayload {
let literalData = """
{
"version": 2,
"heartbeats": []
}
"""
.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(HeartbeatsPayload.dateFormatter)
let heartbeatsPayload = try! decoder.decode(HeartbeatsPayload.self, from: literalData)
return _ObjC_HeartbeatsPayload(heartbeatsPayload)
}
public static var nonEmptyHeartbeatsPayload: _ObjC_HeartbeatsPayload {
let literalData = """
{
"version": 2,
"heartbeats": [
{
"agent": "dummy_agent_1",
"dates": ["2021-11-01", "2021-11-02"]
},
{
"agent": "dummy_agent_2",
"dates": ["2021-11-03"]
}
]
}
"""
.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(HeartbeatsPayload.dateFormatter)
let heartbeatsPayload = try! decoder.decode(HeartbeatsPayload.self, from: literalData)
return _ObjC_HeartbeatsPayload(heartbeatsPayload)
}
@objc(assertEncodedPayloadString:isEqualToLiteralString:withError:)
public static func assertEqualPayloadStrings(_ encoded: String, _ literal: String) throws {
var encodedData = Data(base64URLEncoded: encoded)!
if encodedData.count > 0 {
encodedData = try! encodedData.unzipped()
}
let literalData = literal.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(HeartbeatsPayload.dateFormatter)
let payloadFromEncoded = try? decoder.decode(HeartbeatsPayload.self, from: encodedData)
let payloadFromLiteral = try? decoder.decode(HeartbeatsPayload.self, from: literalData)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(HeartbeatsPayload.dateFormatter)
encoder.outputFormatting = .prettyPrinted
let payloadDataFromEncoded = try! encoder.encode(payloadFromEncoded)
let payloadDataFromLiteral = try! encoder.encode(payloadFromLiteral)
assert(
payloadFromEncoded == payloadFromLiteral,
"""
Mismatched payloads!
Payload 1:
\(String(data: payloadDataFromEncoded, encoding: .utf8) ?? "")
Payload 2:
\(String(data: payloadDataFromLiteral, encoding: .utf8) ?? "")
"""
)
}
/// Removes all underlying storage containers used by the module.
/// - Throws: An error if the storage container could not be removed.
public static func removeUnderlyingHeartbeatStorageContainers() throws {
#if os(tvOS)
UserDefaults().removePersistentDomain(forName: Constants.heartbeatUserDefaultsSuiteName)
#else
let applicationSupportDirectory = FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let heartbeatsDirectoryURL = applicationSupportDirectory
.appendingPathComponent(
Constants.heartbeatFileStorageDirectoryPath, isDirectory: true
)
do {
try FileManager.default.removeItem(at: heartbeatsDirectoryURL)
} catch CocoaError.fileNoSuchFile {
// Do nothing.
} catch {
throw error
}
#endif // os(tvOS)
}
}
#endif // ENABLE_FIREBASE_CORE_INTERNAL_TESTING_UTILS

View File

@ -0,0 +1,180 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// A type that can perform atomic operations using block-based transformations.
protocol HeartbeatStorageProtocol {
func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?)
func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?)
func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws
-> HeartbeatsBundle?
}
/// Thread-safe storage object designed for transforming heartbeat data that is persisted to disk.
final class HeartbeatStorage: HeartbeatStorageProtocol {
/// The identifier used to differentiate instances.
private let id: String
/// The underlying storage container to read from and write to.
private let storage: Storage
/// The encoder used for encoding heartbeat data.
private let encoder: JSONEncoder = .init()
/// The decoder used for decoding heartbeat data.
private let decoder: JSONDecoder = .init()
/// The queue for synchronizing storage operations.
private let queue: DispatchQueue
/// Designated initializer.
/// - Parameters:
/// - id: A string identifier.
/// - storage: The underlying storage container where heartbeat data is stored.
init(id: String,
storage: Storage) {
self.id = id
self.storage = storage
queue = DispatchQueue(label: "com.heartbeat.storage.\(id)")
}
// MARK: - Instance Management
/// Statically allocated cache of `HeartbeatStorage` instances keyed by string IDs.
private static var cachedInstances: [String: WeakContainer<HeartbeatStorage>] = [:]
/// Gets an existing `HeartbeatStorage` instance with the given `id` if one exists. Otherwise,
/// makes a new instance with the given `id`.
///
/// - Parameter id: A string identifier.
/// - Returns: A `HeartbeatStorage` instance.
static func getInstance(id: String) -> HeartbeatStorage {
if let cachedInstance = cachedInstances[id]?.object {
return cachedInstance
} else {
let newInstance = HeartbeatStorage.makeHeartbeatStorage(id: id)
cachedInstances[id] = WeakContainer(object: newInstance)
return newInstance
}
}
/// Makes a `HeartbeatStorage` instance using a given `String` identifier.
///
/// The created persistent storage object is platform dependent. For tvOS, user defaults
/// is used as the underlying storage container due to system storage limits. For all other
/// platforms,
/// the file system is used.
///
/// - Parameter id: A `String` identifier used to create the `HeartbeatStorage`.
/// - Returns: A `HeartbeatStorage` instance.
private static func makeHeartbeatStorage(id: String) -> HeartbeatStorage {
#if os(tvOS)
let storage = UserDefaultsStorage.makeStorage(id: id)
#else
let storage = FileStorage.makeStorage(id: id)
#endif // os(tvOS)
return HeartbeatStorage(id: id, storage: storage)
}
deinit {
// Removes the instance if it was cached.
Self.cachedInstances.removeValue(forKey: id)
}
// MARK: - HeartbeatStorageProtocol
/// Synchronously reads from and writes to storage using the given transform block.
/// - Parameter transform: A block to transform the currently stored heartbeats bundle to a new
/// heartbeats bundle value.
func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) {
queue.sync {
let oldHeartbeatsBundle = try? load(from: storage)
let newHeartbeatsBundle = transform(oldHeartbeatsBundle)
try? save(newHeartbeatsBundle, to: storage)
}
}
/// Asynchronously reads from and writes to storage using the given transform block.
/// - Parameter transform: A block to transform the currently stored heartbeats bundle to a new
/// heartbeats bundle value.
func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) {
queue.async { [self] in
let oldHeartbeatsBundle = try? load(from: storage)
let newHeartbeatsBundle = transform(oldHeartbeatsBundle)
try? save(newHeartbeatsBundle, to: storage)
}
}
/// Synchronously gets the current heartbeat data from storage and resets the storage using the
/// given transform block.
///
/// This API is like any `getAndSet`-style API in that it gets (and returns) the current value and
/// uses
/// a block to transform the current value (or, soon-to-be old value) to a new value.
///
/// - Parameter transform: An optional block used to reset the currently stored heartbeat.
/// - Returns: The heartbeat data that was stored (before the `transform` was applied).
@discardableResult
func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws
-> HeartbeatsBundle? {
let heartbeatsBundle: HeartbeatsBundle? = try queue.sync {
let oldHeartbeatsBundle = try? load(from: storage)
let newHeartbeatsBundle = transform(oldHeartbeatsBundle)
try save(newHeartbeatsBundle, to: storage)
return oldHeartbeatsBundle
}
return heartbeatsBundle
}
/// Loads and decodes the stored heartbeats bundle from a given storage object.
/// - Parameter storage: The storage container to read from.
/// - Returns: The decoded `HeartbeatsBundle` loaded from storage; `nil` if storage is empty.
/// - Throws: An error if storage could not be read or the data could not be decoded.
private func load(from storage: Storage) throws -> HeartbeatsBundle? {
let data = try storage.read()
if data.isEmpty {
return nil
} else {
let heartbeatData = try data.decoded(using: decoder) as HeartbeatsBundle
return heartbeatData
}
}
/// Saves the encoding of the given value to the given storage container.
/// - Parameters:
/// - heartbeatsBundle: The heartbeats bundle to encode and save.
/// - storage: The storage container to write to.
private func save(_ heartbeatsBundle: HeartbeatsBundle?, to storage: Storage) throws {
if let heartbeatsBundle {
let data = try heartbeatsBundle.encoded(using: encoder)
try storage.write(data)
} else {
try storage.write(nil)
}
}
}
private extension Data {
/// Returns the decoded value of this `Data` using the given decoder. Defaults to `JSONDecoder`.
/// - Returns: The decoded value.
func decoded<T>(using decoder: JSONDecoder = .init()) throws -> T where T: Decodable {
try decoder.decode(T.self, from: self)
}
}
private extension Encodable {
/// Returns the `Data` encoding of this value using the given encoder.
/// - Parameter encoder: An encoder used to encode the value. Defaults to `JSONEncoder`.
/// - Returns: The data encoding of the value.
func encoded(using encoder: JSONEncoder = .init()) throws -> Data {
try encoder.encode(self)
}
}

View File

@ -0,0 +1,151 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// A type that can be converted to a `HeartbeatsPayload`.
protocol HeartbeatsPayloadConvertible {
func makeHeartbeatsPayload() -> HeartbeatsPayload
}
/// A codable collection of heartbeats that has a fixed capacity and optimizations for storing
/// heartbeats of
/// multiple time periods.
struct HeartbeatsBundle: Codable, HeartbeatsPayloadConvertible {
/// The maximum number of heartbeats that can be stored in the buffer.
let capacity: Int
/// A cache used for keeping track of the last heartbeat date recorded for a given time period.
///
/// The cache contains the last added date for each time period. The reason only the date is
/// cached is
/// because it's the only piece of information that should be used by clients to determine whether
/// or not
/// to append a new heartbeat.
private(set) var lastAddedHeartbeatDates: [TimePeriod: Date]
/// A ring buffer of heartbeats.
private var buffer: RingBuffer<Heartbeat>
/// A default cache provider that provides a dictionary of all time periods mapping to a default
/// date.
static var cacheProvider: () -> [TimePeriod: Date] {
let timePeriodsAndDates = TimePeriod.allCases.map { ($0, Date.distantPast) }
return { Dictionary(uniqueKeysWithValues: timePeriodsAndDates) }
}
/// Designated initializer.
/// - Parameters:
/// - capacity: The heartbeat capacity of the initialized collection.
/// - cache: A cache of time periods mapping to dates. Defaults to using static `cacheProvider`.
init(capacity: Int,
cache: [TimePeriod: Date] = cacheProvider()) {
buffer = RingBuffer(capacity: capacity)
self.capacity = capacity
lastAddedHeartbeatDates = cache
}
/// Appends a heartbeat to this collection.
/// - Parameter heartbeat: The heartbeat to append.
mutating func append(_ heartbeat: Heartbeat) {
guard capacity > 0 else {
return // Do not append if capacity is non-positive.
}
do {
// Push the heartbeat to the back of the buffer.
if let overwrittenHeartbeat = try buffer.push(heartbeat) {
// If a heartbeat was overwritten, update the cache to ensure it's date
// is removed.
lastAddedHeartbeatDates = lastAddedHeartbeatDates.mapValues { date in
overwrittenHeartbeat.date == date ? .distantPast : date
}
}
// Update cache with the new heartbeat's date.
for timePeriod in heartbeat.timePeriods {
lastAddedHeartbeatDates[timePeriod] = heartbeat.date
}
} catch let error as RingBuffer<Heartbeat>.Error {
// A ring buffer error occurred while pushing to the buffer so the bundle
// is reset.
self = HeartbeatsBundle(capacity: capacity)
// Create a diagnostic heartbeat to capture the failure and add it to the
// buffer. The failure is added as a key/value pair to the agent string.
// Given that the ring buffer has been reset, it is not expected for the
// second push attempt to fail.
let errorDescription = error.errorDescription.replacingOccurrences(of: " ", with: "-")
let diagnosticHeartbeat = Heartbeat(
agent: "\(heartbeat.agent) error/\(errorDescription)",
date: heartbeat.date,
timePeriods: heartbeat.timePeriods
)
let secondPushAttempt = Result {
try buffer.push(diagnosticHeartbeat)
}
if case .success = secondPushAttempt {
// Update cache with the new heartbeat's date.
for timePeriod in diagnosticHeartbeat.timePeriods {
lastAddedHeartbeatDates[timePeriod] = diagnosticHeartbeat.date
}
}
} catch {
// Ignore other error.
}
}
/// Removes the heartbeat associated with the given date.
/// - Parameter date: The date of the heartbeat needing removal.
/// - Returns: The heartbeat that was removed or `nil` if there was no heartbeat to remove.
@discardableResult
mutating func removeHeartbeat(from date: Date) -> Heartbeat? {
var removedHeartbeat: Heartbeat?
var poppedHeartbeats: [Heartbeat] = []
while let poppedHeartbeat = buffer.pop() {
if poppedHeartbeat.date == date {
removedHeartbeat = poppedHeartbeat
break
}
poppedHeartbeats.append(poppedHeartbeat)
}
for poppedHeartbeat in poppedHeartbeats.reversed() {
do {
try buffer.push(poppedHeartbeat)
} catch {
// Ignore error.
}
}
return removedHeartbeat
}
/// Makes and returns a `HeartbeatsPayload` from this heartbeats bundle.
/// - Returns: A heartbeats payload.
func makeHeartbeatsPayload() -> HeartbeatsPayload {
let agentAndDates = buffer.map { heartbeat in
(heartbeat.agent, [heartbeat.date])
}
let userAgentPayloads = [String: [Date]](agentAndDates, uniquingKeysWith: +)
.map(HeartbeatsPayload.UserAgentPayload.init)
.sorted { $0.agent < $1.agent } // Sort payloads by user agent.
return HeartbeatsPayload(userAgentPayloads: userAgentPayloads)
}
}

View File

@ -0,0 +1,181 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
#if SWIFT_PACKAGE
@_implementationOnly import GoogleUtilities_NSData
#else
@_implementationOnly import GoogleUtilities
#endif // SWIFT_PACKAGE
/// A type that provides a string representation for use in an HTTP header.
public protocol HTTPHeaderRepresentable {
func headerValue() -> String
}
/// A value type representing a payload of heartbeat data intended for sending in network requests.
///
/// This type's structure is optimized for type-safe encoding into a HTTP payload format.
/// The current encoding format for the payload's current version is:
///
/// {
/// "version": 2,
/// "heartbeats": [
/// {
/// "agent": "dummy_agent_1",
/// "dates": ["2021-11-01", "2021-11-02"]
/// },
/// {
/// "agent": "dummy_agent_2",
/// "dates": ["2021-11-03"]
/// }
/// ]
/// }
///
public struct HeartbeatsPayload: Codable, Sendable {
/// The version of the payload. See go/firebase-apple-heartbeats for details regarding current
/// version.
static let version: Int = 2
/// A payload component composed of a user agent and array of dates (heartbeats).
struct UserAgentPayload: Codable {
/// An anonymous agent string.
let agent: String
/// An array of dates where each date represents a "heartbeat".
let dates: [Date]
}
/// An array of user agent payloads.
let userAgentPayloads: [UserAgentPayload]
/// The version of the payload structure.
let version: Int
/// Alternative keys for properties so encoding follows platform-wide payload structure.
enum CodingKeys: String, CodingKey {
case userAgentPayloads = "heartbeats"
case version
}
/// Designated initializer.
/// - Parameters:
/// - userAgentPayloads: An array of payloads containing heartbeat data corresponding to a
/// given user agent.
/// - version: A version of the payload. Defaults to the static default.
init(userAgentPayloads: [UserAgentPayload] = [], version: Int = version) {
self.userAgentPayloads = userAgentPayloads
self.version = version
}
/// A Boolean value indicating whether the payload is empty.
public var isEmpty: Bool {
userAgentPayloads.isEmpty
}
}
// MARK: - HTTPHeaderRepresentable
extension HeartbeatsPayload: HTTPHeaderRepresentable {
/// Returns a processed payload string intended for use in a HTTP header.
/// - Returns: A string value from the heartbeats payload.
public func headerValue() -> String {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(Self.dateFormatter)
#if DEBUG
// Sort keys in debug builds to simplify output comparisons in unit tests.
encoder.outputFormatting = .sortedKeys
#endif // DEBUG
guard let data = try? encoder.encode(self) else {
// If encoding fails, fall back to encoding with an empty payload.
return Self.emptyPayload.headerValue()
}
do {
let gzippedData = try data.zipped()
return gzippedData.base64URLEncodedString()
} catch {
// If gzipping fails, fall back to encoding with base64URL.
return data.base64URLEncodedString()
}
}
}
// MARK: - Static Defaults
extension HeartbeatsPayload {
/// Convenience instance that represents an empty payload.
static let emptyPayload = HeartbeatsPayload()
/// A default date formatter that uses `yyyy-MM-dd` format.
public static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
}
// MARK: - Equatable
extension HeartbeatsPayload: Equatable {}
extension HeartbeatsPayload.UserAgentPayload: Equatable {}
// MARK: - Data
public extension Data {
/// Returns a Base-64 URL-safe encoded string.
///
/// - parameter options: The options to use for the encoding. Default value is `[]`.
/// - returns: The Base-64 URL-safe encoded string.
func base64URLEncodedString(options: Data.Base64EncodingOptions = []) -> String {
base64EncodedString()
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "=", with: "")
}
/// Initialize a `Data` from a Base-64 URL encoded String using the given options.
///
/// Returns nil when the input is not recognized as valid Base-64.
/// - parameter base64URLString: The string to parse.
/// - parameter options: Encoding options. Default value is `[]`.
init?(base64URLEncoded base64URLString: String, options: Data.Base64DecodingOptions = []) {
var base64Encoded = base64URLString
.replacingOccurrences(of: "_", with: "/")
.replacingOccurrences(of: "-", with: "+")
// Pad the string with "=" signs until the string's length is a multiple of 4.
while !base64Encoded.count.isMultiple(of: 4) {
base64Encoded.append("=")
}
self.init(base64Encoded: base64Encoded, options: options)
}
/// Returns the compressed data.
/// - Returns: The compressed data.
/// - Throws: An error if compression failed.
func zipped() throws -> Data {
try NSData.gul_data(byGzippingData: self)
}
/// Returns the uncompressed data.
/// - Returns: The decompressed data.
/// - Throws: An error if decompression failed.
func unzipped() throws -> Data {
try NSData.gul_data(byInflatingGzippedData: self)
}
}

View File

@ -0,0 +1,111 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// A generic circular queue structure.
struct RingBuffer<Element>: Sequence {
/// An array of heartbeats treated as a circular queue and initialized with a fixed capacity.
private var circularQueue: [Element?]
/// The current "tail" and insert point for the `circularQueue`.
private var tailIndex: Array<Element?>.Index
/// Error types for `RingBuffer` operations.
enum Error: LocalizedError {
case outOfBoundsPush(pushIndex: Array<Element?>.Index, endIndex: Array<Element?>.Index)
var errorDescription: String {
switch self {
case let .outOfBoundsPush(pushIndex, endIndex):
return "Out-of-bounds push at index \(pushIndex) to ring buffer with" +
"end index of \(endIndex)."
}
}
}
/// Designated initializer.
/// - Parameter capacity: An `Int` representing the capacity.
init(capacity: Int) {
circularQueue = Array(repeating: nil, count: capacity)
tailIndex = circularQueue.startIndex
}
/// Pushes an element to the back of the buffer, returning the element (`Element?`) that was
/// overwritten.
/// - Parameter element: The element to push to the back of the buffer.
/// - Returns: The element that was overwritten or `nil` if nothing was overwritten.
/// - Complexity: O(1)
@discardableResult
mutating func push(_ element: Element) throws -> Element? {
guard circularQueue.count > 0 else {
// Do not push if `circularQueue` is a fixed empty array.
return nil
}
guard circularQueue.indices.contains(tailIndex) else {
// We have somehow entered an invalid state (#10025).
throw Self.Error.outOfBoundsPush(
pushIndex: tailIndex,
endIndex: circularQueue.endIndex
)
}
let replaced = circularQueue[tailIndex]
circularQueue[tailIndex] = element
// Increment index, wrapping around to the start if needed.
tailIndex += 1
if tailIndex >= circularQueue.endIndex {
tailIndex = circularQueue.startIndex
}
return replaced
}
/// Pops an element from the back of the buffer, returning the element (`Element?`) that was
/// popped.
/// - Returns: The element that was popped or `nil` if there was no element to pop.
/// - Complexity: O(1)
@discardableResult
mutating func pop() -> Element? {
guard circularQueue.count > 0 else {
// Do not pop if `circularQueue` is a fixed empty array.
return nil
}
// Decrement index, wrapping around to the back if needed.
tailIndex -= 1
if tailIndex < circularQueue.startIndex {
tailIndex = circularQueue.endIndex - 1
}
guard let popped = circularQueue[tailIndex] else {
return nil // There is no element to pop.
}
circularQueue[tailIndex] = nil
return popped
}
func makeIterator() -> IndexingIterator<[Element]> {
circularQueue
.compactMap { $0 } // Remove `nil` elements.
.makeIterator()
}
}
// MARK: - Codable
extension RingBuffer: Codable where Element: Codable {}

View File

@ -0,0 +1,145 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// A type that reads from and writes to an underlying storage container.
protocol Storage {
/// Reads and returns the data stored by this storage type.
/// - Returns: The data read from storage.
/// - Throws: An error if the read failed.
func read() throws -> Data
/// Writes the given data to this storage type.
/// - Throws: An error if the write failed.
func write(_ data: Data?) throws
}
/// Error types for `Storage` operations.
enum StorageError: Error {
case readError
case writeError
}
// MARK: - FileStorage
/// A object that provides API for reading and writing to a file system resource.
final class FileStorage: Storage {
/// A file system URL to the underlying file resource.
private let url: URL
/// The file manager used to perform file system operations.
private let fileManager: FileManager
/// Designated initializer.
/// - Parameters:
/// - url: A file system URL for the underlying file resource.
/// - fileManager: A file manager. Defaults to `default` manager.
init(url: URL, fileManager: FileManager = .default) {
self.url = url
self.fileManager = fileManager
}
/// Reads and returns the data from this object's associated file resource.
///
/// - Returns: The data stored on disk.
/// - Throws: An error if reading the contents of the file resource fails (i.e. file doesn't
/// exist).
func read() throws -> Data {
do {
return try Data(contentsOf: url)
} catch {
throw StorageError.readError
}
}
/// Writes the given data to this object's associated file resource.
///
/// When the given `data` is `nil`, this object's associated file resource is emptied.
///
/// - Parameter data: The `Data?` to write to this object's associated file resource.
func write(_ data: Data?) throws {
do {
try createDirectories(in: url.deletingLastPathComponent())
if let data {
try data.write(to: url, options: .atomic)
} else {
let emptyData = Data()
try emptyData.write(to: url, options: .atomic)
}
} catch {
throw StorageError.writeError
}
}
/// Creates all directories in the given file system URL.
///
/// If the directory for the given URL already exists, the error is ignored because the directory
/// has already been created.
///
/// - Parameter url: The URL to create directories in.
private func createDirectories(in url: URL) throws {
do {
try fileManager.createDirectory(
at: url,
withIntermediateDirectories: true
)
} catch CocoaError.fileWriteFileExists {
// Directory already exists.
} catch { throw error }
}
}
// MARK: - UserDefaultsStorage
/// A object that provides API for reading and writing to a user defaults resource.
final class UserDefaultsStorage: Storage {
/// The underlying defaults container.
private let defaults: UserDefaults
/// The key mapping to the object's associated resource in `defaults`.
private let key: String
/// Designated initializer.
/// - Parameters:
/// - defaults: The defaults container.
/// - key: The key mapping to the value stored in the defaults container.
init(defaults: UserDefaults, key: String) {
self.defaults = defaults
self.key = key
}
/// Reads and returns the data from this object's associated defaults resource.
///
/// - Returns: The data stored on disk.
/// - Throws: An error if no data has been stored to the defaults container.
func read() throws -> Data {
if let data = defaults.data(forKey: key) {
return data
} else {
throw StorageError.readError
}
}
/// Writes the given data to this object's associated defaults.
///
/// When the given `data` is `nil`, the associated default is removed.
///
/// - Parameter data: The `Data?` to write to this object's associated defaults.
func write(_ data: Data?) throws {
if let data {
defaults.set(data, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}
}

View File

@ -0,0 +1,66 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
private enum Constants {
/// The name of the file system directory where heartbeat data is stored.
static let heartbeatFileStorageDirectoryPath = "google-heartbeat-storage"
/// The name of the user defaults suite where heartbeat data is stored.
static let heartbeatUserDefaultsSuiteName = "com.google.heartbeat.storage"
}
/// A factory type for `Storage`.
protocol StorageFactory {
static func makeStorage(id: String) -> Storage
}
// MARK: - FileStorage + StorageFactory
extension FileStorage: StorageFactory {
static func makeStorage(id: String) -> Storage {
let rootDirectory = FileManager.default.applicationSupportDirectory
let heartbeatDirectoryPath = Constants.heartbeatFileStorageDirectoryPath
// Sanitize the `id` so the heartbeat file name does not include a ":".
let sanitizedID = id.replacingOccurrences(of: ":", with: "_")
let heartbeatFilePath = "heartbeats-\(sanitizedID)"
let storageURL = rootDirectory
.appendingPathComponent(heartbeatDirectoryPath, isDirectory: true)
.appendingPathComponent(heartbeatFilePath, isDirectory: false)
return FileStorage(url: storageURL)
}
}
extension FileManager {
var applicationSupportDirectory: URL {
urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
}
}
// MARK: - UserDefaultsStorage + StorageFactory
extension UserDefaultsStorage: StorageFactory {
static func makeStorage(id: String) -> Storage {
let suiteName = Constants.heartbeatUserDefaultsSuiteName
// It's safe to force unwrap the below defaults instance because the
// initializer only returns `nil` when the bundle id or `globalDomain`
// is passed in as the `suiteName`.
let defaults = UserDefaults(suiteName: suiteName)!
let key = "heartbeats-\(id)"
return UserDefaultsStorage(defaults: defaults, key: key)
}
}

View File

@ -0,0 +1,20 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// A structure used to weakly box reference types.
struct WeakContainer<Object: AnyObject> {
weak var object: Object?
}

View File

@ -0,0 +1,58 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// An object that provides API to log and flush heartbeats from a synchronized storage container.
@objc(FIRHeartbeatController)
@objcMembers
public class _ObjC_HeartbeatController: NSObject {
/// The underlying Swift object.
private let heartbeatController: HeartbeatController
/// Public initializer.
/// - Parameter id: The `id` to associate this controller's heartbeat storage with.
public init(id: String) {
heartbeatController = HeartbeatController(id: id)
}
/// Asynchronously logs a new heartbeat, if needed.
///
/// - Note: This API is thread-safe.
/// - Parameter agent: The string agent (i.e. Firebase User Agent) to associate the logged
/// heartbeat with.
public func log(_ agent: String) {
heartbeatController.log(agent)
}
/// Synchronously flushes heartbeats from storage into a heartbeats payload.
///
/// - Note: This API is thread-safe.
/// - Returns: A heartbeats payload for the flushed heartbeat(s).
public func flush() -> _ObjC_HeartbeatsPayload {
let heartbeatsPayload = heartbeatController.flush()
return _ObjC_HeartbeatsPayload(heartbeatsPayload)
}
/// Synchronously flushes the heartbeat for today.
///
/// If no heartbeat was logged today, the returned payload is empty.
///
/// - Note: This API is thread-safe.
/// - Returns: A heartbeats payload for the flushed heartbeat.
public func flushHeartbeatFromToday() -> _ObjC_HeartbeatsPayload {
let heartbeatsPayload = heartbeatController.flushHeartbeatFromToday()
return _ObjC_HeartbeatsPayload(heartbeatsPayload)
}
}

View File

@ -0,0 +1,40 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// A model object representing a payload of heartbeat data intended for sending in network
/// requests.
@objc(FIRHeartbeatsPayload)
public class _ObjC_HeartbeatsPayload: NSObject, HTTPHeaderRepresentable {
/// The underlying Swift structure.
private let heartbeatsPayload: HeartbeatsPayload
/// Designated initializer.
/// - Parameter heartbeatsPayload: A native-Swift heartbeats payload.
public init(_ heartbeatsPayload: HeartbeatsPayload) {
self.heartbeatsPayload = heartbeatsPayload
}
/// Returns a processed payload string intended for use in a HTTP header.
/// - Returns: A string value from the heartbeats payload.
@objc public func headerValue() -> String {
heartbeatsPayload.headerValue()
}
/// A Boolean value indicating whether the payload is empty.
@objc public var isEmpty: Bool {
heartbeatsPayload.isEmpty
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>

202
Pods/FirebaseCoreInternal/LICENSE generated Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

302
Pods/FirebaseCoreInternal/README.md generated Normal file
View File

@ -0,0 +1,302 @@
<p align="center">
<a href="https://cocoapods.org/pods/Firebase">
<img src="https://img.shields.io/github/v/release/Firebase/firebase-ios-sdk?style=flat&label=CocoaPods"/>
</a>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/github/v/release/Firebase/firebase-ios-sdk?style=flat&label=Swift%20Package%20Index&color=red"/>
</a>
<a href="https://cocoapods.org/pods/Firebase">
<img src="https://img.shields.io/github/license/Firebase/firebase-ios-sdk?style=flat"/>
</a><br/>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffirebase%2Ffirebase-ios-sdk%2Fbadge%3Ftype%3Dplatforms"/>
</a>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffirebase%2Ffirebase-ios-sdk%2Fbadge%3Ftype%3Dswift-versions"/>
</a>
</p>
# Firebase Apple Open Source Development
This repository contains the source code for all Apple platform Firebase SDKs except FirebaseAnalytics.
Firebase is an app development platform with tools to help you build, grow, and
monetize your app. More information about Firebase can be found on the
[official Firebase website](https://firebase.google.com).
## Installation
See the subsections below for details about the different installation methods. Where
available, it's recommended to install any libraries with a `Swift` suffix to get the
best experience when writing your app in Swift.
1. [Standard pod install](#standard-pod-install)
2. [Swift Package Manager](#swift-package-manager)
3. [Installing from the GitHub repo](#installing-from-github)
4. [Experimental Carthage](#carthage-ios-only)
### Standard pod install
For instructions on the standard pod install, visit:
[https://firebase.google.com/docs/ios/setup](https://firebase.google.com/docs/ios/setup).
### Swift Package Manager
Instructions for [Swift Package Manager](https://swift.org/package-manager/) support can be
found in the [SwiftPackageManager.md](SwiftPackageManager.md) Markdown file.
### Installing from GitHub
These instructions can be used to access the Firebase repo at other branches,
tags, or commits.
#### Background
See [the Podfile Syntax Reference](https://guides.cocoapods.org/syntax/podfile.html#pod)
for instructions and options about overriding pod source locations.
#### Accessing Firebase Source Snapshots
All official releases are tagged in this repo and available via CocoaPods. To access a local
source snapshot or unreleased branch, use Podfile directives like the following:
To access FirebaseFirestore via a branch:
```ruby
pod 'FirebaseCore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'main'
pod 'FirebaseFirestore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'main'
```
To access FirebaseMessaging via a checked-out version of the firebase-ios-sdk repo:
```ruby
pod 'FirebaseCore', :path => '/path/to/firebase-ios-sdk'
pod 'FirebaseMessaging', :path => '/path/to/firebase-ios-sdk'
```
### Carthage (iOS only)
Instructions for the experimental Carthage distribution can be found at
[Carthage.md](Carthage.md).
### Using Firebase from a Framework or a library
For details on using Firebase from a Framework or a library, refer to [firebase_in_libraries.md](docs/firebase_in_libraries.md).
## Development
To develop Firebase software in this repository, ensure that you have at least
the following software:
* Xcode 15.2 (or later)
CocoaPods is still the canonical way to develop, but much of the repo now supports
development with Swift Package Manager.
### CocoaPods
Install the following:
* CocoaPods 1.12.0 (or later)
* [CocoaPods generate](https://github.com/square/cocoapods-generate)
For the pod that you want to develop:
```ruby
pod gen Firebase{name here}.podspec --local-sources=./ --auto-open --platforms=ios
```
Note: If the CocoaPods cache is out of date, you may need to run
`pod repo update` before the `pod gen` command.
Note: Set the `--platforms` option to `macos` or `tvos` to develop/test for
those platforms. Since 10.2, Xcode does not properly handle multi-platform
CocoaPods workspaces.
Firestore has a self-contained Xcode project. See
[Firestore/README](Firestore/README.md) Markdown file.
#### Development for Catalyst
* `pod gen {name here}.podspec --local-sources=./ --auto-open --platforms=ios`
* Check the Mac box in the App-iOS Build Settings
* Sign the App in the Settings Signing & Capabilities tab
* Click Pods in the Project Manager
* Add Signing to the iOS host app and unit test targets
* Select the Unit-unit scheme
* Run it to build and test
Alternatively, disable signing in each target:
* Go to Build Settings tab
* Click `+`
* Select `Add User-Defined Setting`
* Add `CODE_SIGNING_REQUIRED` setting with a value of `NO`
### Swift Package Manager
* To enable test schemes: `./scripts/setup_spm_tests.sh`
* `open Package.swift` or double click `Package.swift` in Finder.
* Xcode will open the project
* Choose a scheme for a library to build or test suite to run
* Choose a target platform by selecting the run destination along with the scheme
### Adding a New Firebase Pod
Refer to [AddNewPod](AddNewPod.md) Markdown file for details.
### Managing Headers and Imports
For information about managing headers and imports, see [HeadersImports](HeadersImports.md) Markdown file.
### Code Formatting
To ensure that the code is formatted consistently, run the script
[./scripts/check.sh](https://github.com/firebase/firebase-ios-sdk/blob/main/scripts/check.sh)
before creating a pull request (PR).
GitHub Actions will verify that any code changes are done in a style-compliant
way. Install `clang-format` and `mint`:
```console
brew install clang-format@18
brew install mint
```
### Running Unit Tests
Select a scheme and press Command-u to build a component and run its unit tests.
### Running Sample Apps
To run the sample apps and integration tests, you'll need a valid
`GoogleService-Info.plist
` file. The Firebase Xcode project contains dummy plist
files without real values, but they can be replaced with real plist files. To get your own
`GoogleService-Info.plist` files:
1. Go to the [Firebase Console](https://console.firebase.google.com/)
2. Create a new Firebase project, if you don't already have one
3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
identifier (e.g., `com.google.Database-Example`)
4. Download the resulting `GoogleService-Info.plist` and add it to the Xcode project.
### Coverage Report Generation
For coverage report generation instructions, see [scripts/code_coverage_report/README](scripts/code_coverage_report/README.md) Markdown file.
## Specific Component Instructions
See the sections below for any special instructions for those components.
### Firebase Auth
For specific Firebase Auth development, refer to the [Auth Sample README](FirebaseAuth/Tests/Sample/README.md) for instructions about
building and running the FirebaseAuth pod along with various samples and tests.
### Firebase Database
The Firebase Database Integration tests can be run against a locally running Database Emulator
or against a production instance.
To run against a local emulator instance, invoke `./scripts/run_database_emulator.sh start` before
running the integration test.
To run against a production instance, provide a valid `GoogleServices-Info.plist` and copy it to
`FirebaseDatabase/Tests/Resources/GoogleService-Info.plist`. Your Security Rule must be set to
[public](https://firebase.google.com/docs/database/security/quickstart) while your tests are
running.
### Firebase Dynamic Links
Firebase Dynamic Links is **deprecated** and should not be used in new projects. The service will shut down on August 25, 2025.
Please see our [Dynamic Links Deprecation FAQ documentation](https://firebase.google.com/support/dynamic-links-faq) for more guidance.
### Firebase Performance Monitoring
For specific Firebase Performance Monitoring development, see
[the Performance README](FirebasePerformance/README.md) for instructions about building the SDK
and [the Performance TestApp README](FirebasePerformance/Tests/TestApp/README.md) for instructions about
integrating Performance with the dev test App.
### Firebase Storage
To run the Storage Integration tests, follow the instructions in
[StorageIntegration.swift](FirebaseStorage/Tests/Integration/StorageIntegration.swift).
#### Push Notifications
Push notifications can only be delivered to specially provisioned App IDs in the developer portal.
In order to test receiving push notifications, you will need to:
1. Change the bundle identifier of the sample app to something you own in your Apple Developer
account and enable that App ID for push notifications.
2. You'll also need to
[upload your APNs Provider Authentication Key or certificate to the
Firebase Console](https://firebase.google.com/docs/cloud-messaging/ios/certs)
at **Project Settings > Cloud Messaging > [Your Firebase App]**.
3. Ensure your iOS device is added to your Apple Developer portal as a test device.
#### iOS Simulator
The iOS Simulator cannot register for remote notifications and will not receive push notifications.
To receive push notifications, follow the steps above and run the app on a physical device.
### Vertex AI for Firebase
See the [Vertex AI for Firebase README](FirebaseVertexAI#development) for
instructions about building and testing the SDK.
## Building with Firebase on Apple platforms
Firebase provides official beta support for macOS, Catalyst, and tvOS. visionOS and watchOS
are community supported. Thanks to community contributions for many of the multi-platform PRs.
At this time, most of Firebase's products are available across Apple platforms. There are still
a few gaps, especially on visionOS and watchOS. For details about the current support matrix, see
[this chart](https://firebase.google.com/docs/ios/learn-more#firebase_library_support_by_platform)
in Firebase's documentation.
### visionOS
Where supported, visionOS works as expected with the exception of Firestore via Swift Package
Manager where it is required to use the source distribution.
To enable the Firestore source distribution, quit Xcode and open the desired
project from the command line with the `FIREBASE_SOURCE_FIRESTORE` environment
variable: `open --env FIREBASE_SOURCE_FIRESTORE /path/to/project.xcodeproj`.
To go back to using the binary distribution of Firestore, quit Xcode and open
Xcode like normal, without the environment variable.
### watchOS
Thanks to contributions from the community, many of Firebase SDKs now compile, run unit tests, and
work on watchOS. See the [Independent Watch App Sample](Example/watchOSSample).
Keep in mind that watchOS is not officially supported by Firebase. While we can catch basic unit
test issues with GitHub Actions, there may be some changes where the SDK no longer works as expected
on watchOS. If you encounter this, please
[file an issue](https://github.com/firebase/firebase-ios-sdk/issues).
During app setup in the console, you may get to a step that mentions something like "Checking if the
app has communicated with our servers". This relies on Analytics and will not work on watchOS.
**It's safe to ignore the message and continue**, the rest of the SDKs will work as expected.
#### Additional Crashlytics Notes
* watchOS has limited support. Due to watchOS restrictions, mach exceptions and signal crashes are
not recorded. (Crashes in SwiftUI are generated as mach exceptions, so will not be recorded)
## Combine
Thanks to contributions from the community, _FirebaseCombineSwift_ contains support for Apple's Combine
framework. This module is currently under development and not yet supported for use in production
environments. For more details, please refer to the [docs](FirebaseCombineSwift/README.md).
## Roadmap
See [Roadmap](ROADMAP.md) for more about the Firebase Apple SDK Open Source
plans and directions.
## Contributing
See [Contributing](CONTRIBUTING.md) for more information on contributing to the Firebase
Apple SDK.
## License
The contents of this repository are licensed under the
[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Your use of Firebase is governed by the
[Terms of Service for Firebase Services](https://firebase.google.com/terms/).